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
3c09796iko
app/src/main/java/soy/iko/opencode/MainActivity.kt+7−1
@@ -2,6 +2,7 @@ package soy.iko.opencode
2
2
3import android.os.Bundle
3import android.os.Bundle
4import androidx.activity.ComponentActivity
4import androidx.activity.ComponentActivity
5+import androidx.activity.SystemBarStyle
5import androidx.activity.compose.setContent
6import androidx.activity.compose.setContent
6import androidx.activity.enableEdgeToEdge
7import androidx.activity.enableEdgeToEdge
7import androidx.compose.runtime.getValue
8import androidx.compose.runtime.getValue
@@ -12,7 +13,12 @@ import soy.iko.opencode.ui.theme.OpencodeTheme
12
13
13class MainActivity : ComponentActivity() {
14class MainActivity : ComponentActivity() {
14 override fun onCreate(savedInstanceState: Bundle?) {
15 override fun onCreate(savedInstanceState: Bundle?) {
15− enableEdgeToEdge()
16+ enableEdgeToEdge(
17+ statusBarStyle = SystemBarStyle.auto(
18+ lightScrim = 0xFF0F1115.toInt(),
19+ darkScrim = 0xFF0F1115.toInt(),
20+ ),
21+ )
16 super.onCreate(savedInstanceState)
22 super.onCreate(savedInstanceState)
17 setContent {
23 setContent {
18 OpencodeTheme {
24 OpencodeTheme {
app/src/main/java/soy/iko/opencode/core/OpencodeCore.kt+4
@@ -79,6 +79,7 @@ class OpencodeCore(
79 messages.clear()
79 messages.clear()
80 screen = Screen.Chat
80 screen = Screen.Chat
81 loading = true
81 loading = true
82+ generating = false
82 error = null
83 error = null
83 emit()
84 emit()
84 scope.launch { loadMessages(event.id) }
85 scope.launch { loadMessages(event.id) }
@@ -189,12 +190,14 @@ class OpencodeCore(
189 } else {
190 } else {
190 messages.firstOrNull { it.id == user.id }?.status = MessageStatus.Failed
191 messages.firstOrNull { it.id == user.id }?.status = MessageStatus.Failed
191 generating = false
192 generating = false
193+ messages.forEach { it.streaming = false }
192 error = "Server returned status ${resp.code}"
194 error = "Server returned status ${resp.code}"
193 }
195 }
194 },
196 },
195 onFailure = { e ->
197 onFailure = { e ->
196 messages.firstOrNull { it.id == user.id }?.status = MessageStatus.Failed
198 messages.firstOrNull { it.id == user.id }?.status = MessageStatus.Failed
197 generating = false
199 generating = false
200+ messages.forEach { it.streaming = false }
198 error = "Request failed: ${e.message}"
201 error = "Request failed: ${e.message}"
199 },
202 },
200 )
203 )
@@ -294,6 +297,7 @@ class OpencodeCore(
294 onFailure = { e ->
297 onFailure = { e ->
295 if (!isCurrent(sessionId)) { loading = false; return }
298 if (!isCurrent(sessionId)) { loading = false; return }
296 loading = false
299 loading = false
300+ generating = false
297 error = "Request failed: ${e.message}"
301 error = "Request failed: ${e.message}"
298 emit()
302 emit()
299 },
303 },
app/src/main/java/soy/iko/opencode/ui/components/MessageBubble.kt+2−1
@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.widthIn
17import androidx.compose.foundation.shape.RoundedCornerShape
17import androidx.compose.foundation.shape.RoundedCornerShape
18import androidx.compose.material.icons.Icons
18import androidx.compose.material.icons.Icons
19import androidx.compose.material.icons.filled.Build
19import androidx.compose.material.icons.filled.Build
20+import androidx.compose.material.icons.filled.Check
20import androidx.compose.material.icons.filled.ErrorOutline
21import androidx.compose.material.icons.filled.ErrorOutline
21import androidx.compose.material.icons.filled.ExpandLess
22import androidx.compose.material.icons.filled.ExpandLess
22import androidx.compose.material.icons.filled.ExpandMore
23import androidx.compose.material.icons.filled.ExpandMore
@@ -201,7 +202,7 @@ private fun StatusLine(status: MessageStatus) {
201 val (icon, label, tint) = when (status) {
202 val (icon, label, tint) = when (status) {
202 MessageStatus.Pending -> Triple(Icons.Filled.Schedule, "Sending…", MaterialTheme.colorScheme.onPrimaryContainer)
203 MessageStatus.Pending -> Triple(Icons.Filled.Schedule, "Sending…", MaterialTheme.colorScheme.onPrimaryContainer)
203 MessageStatus.Failed -> Triple(Icons.Filled.ErrorOutline, "Failed to send", MaterialTheme.colorScheme.error)
204 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)
205 }
206 }
206 Row(
207 Row(
207 verticalAlignment = Alignment.CenterVertically,
208 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
19import androidx.compose.material.icons.automirrored.filled.ArrowBack
19import androidx.compose.material.icons.automirrored.filled.ArrowBack
20import androidx.compose.material.icons.filled.Add
20import androidx.compose.material.icons.filled.Add
21import androidx.compose.material.icons.filled.ChatBubbleOutline
21import androidx.compose.material.icons.filled.ChatBubbleOutline
22+import androidx.compose.material.icons.filled.ChevronRight
22import androidx.compose.material.icons.filled.DeleteOutline
23import androidx.compose.material.icons.filled.DeleteOutline
23import androidx.compose.material.icons.filled.Refresh
24import androidx.compose.material.icons.filled.Refresh
24import androidx.compose.material3.AlertDialog
25import androidx.compose.material3.AlertDialog
26+import androidx.compose.material3.CircularProgressIndicator
25import androidx.compose.material3.ExperimentalMaterial3Api
27import androidx.compose.material3.ExperimentalMaterial3Api
26import androidx.compose.material3.ExtendedFloatingActionButton
28import androidx.compose.material3.ExtendedFloatingActionButton
27import androidx.compose.material3.HorizontalDivider
29import androidx.compose.material3.HorizontalDivider
@@ -31,6 +33,7 @@ import androidx.compose.material3.LinearProgressIndicator
31import androidx.compose.material3.MaterialTheme
33import androidx.compose.material3.MaterialTheme
32import androidx.compose.material3.OutlinedTextField
34import androidx.compose.material3.OutlinedTextField
33import androidx.compose.material3.Scaffold
35import androidx.compose.material3.Scaffold
36+import androidx.compose.material3.Surface
34import androidx.compose.material3.SwipeToDismissBox
37import androidx.compose.material3.SwipeToDismissBox
35import androidx.compose.material3.SwipeToDismissBoxValue
38import androidx.compose.material3.SwipeToDismissBoxValue
36import androidx.compose.material3.Text
39import androidx.compose.material3.Text
@@ -117,6 +120,9 @@ fun SessionsScreen(state: UiState, dispatch: (Event) -> Unit) {
117 },
120 },
118 ) { padding ->
121 ) { padding ->
119 Column(Modifier.fillMaxSize().padding(padding)) {
122 Column(Modifier.fillMaxSize().padding(padding)) {
123+ if (!state.sseConnected) {
124+ ReconnectingBanner()
125+ }
120 if (state.loading && !isPullRefreshing) {
126 if (state.loading && !isPullRefreshing) {
121 LinearProgressIndicator(Modifier.fillMaxWidth())
127 LinearProgressIndicator(Modifier.fillMaxWidth())
122 }
128 }
@@ -217,6 +223,12 @@ private fun SessionRow(session: SessionView, onOpen: () -> Unit, onDelete: () ->
217 overflow = TextOverflow.Ellipsis,
223 overflow = TextOverflow.Ellipsis,
218 )
224 )
219 }
225 }
226+ Icon(
227+ Icons.Filled.ChevronRight,
228+ contentDescription = null,
229+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
230+ modifier = Modifier.padding(horizontal = 4.dp),
231+ )
220 IconButton(onClick = onDelete) {
232 IconButton(onClick = onDelete) {
221 Icon(
233 Icon(
222 Icons.Filled.DeleteOutline,
234 Icons.Filled.DeleteOutline,
@@ -245,6 +257,32 @@ private fun SwipeDeleteBackground() {
245 }
257 }
246}
258}
247
259
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+
248/** Shorten a session ID for display: "abc12345…" instead of the full UUID. */
286/** Shorten a session ID for display: "abc12345…" instead of the full UUID. */
249private fun shortSessionId(id: String): String =
287private fun shortSessionId(id: String): String =
250 if (id.length > 12) id.take(8) + "…" else id
288 if (id.length > 12) id.take(8) + "…" else id