git.iko.soy/opencode-android
on master help sign in
UX/UI: disconnect confirmation, tool call count summary, disable connect when auth creds empty

Bug fixes: code background contrast per bubble type, sort sessions after create, clear error on NavigateToSessions

commit0c49daf597927c94f73b64db97a7bfb16b3960aa authoriko <mail@iko.soy> dateThu 9 Jul 2026, 03:43 (3 months ago) parent29df003
4 files changed+48−3
app/src/main/java/soy/iko/opencode/core/OpencodeCore.kt+7 app/src/main/java/soy/iko/opencode/ui/OpencodeApp.kt+24−1 app/src/main/java/soy/iko/opencode/ui/components/MessageBubble.kt+13−1 app/src/main/java/soy/iko/opencode/ui/screens/ConnectScreen.kt+4−1
app/src/main/java/soy/iko/opencode/core/OpencodeCore.kt+7
@@ -189,6 +189,9 @@ class OpencodeCore(
189189 removedMessageIds.clear()
190190 draft = ""
191191 generating = false
192+ // Clear any error from the chat (e.g. a failed send) so it doesn't
193+ // linger on the sessions list after the user navigates back.
194+ error = null
192195 loading = true
193196 emit()
194197 connectJob?.cancel()
@@ -358,6 +361,10 @@ class OpencodeCore(
358361 if (session != null) {
359362 if (sessions.none { it.id == session.id }) {
360363 sessions.add(0, SessionView(session.id, displayTitle(session.title)))
364+ // Restore the alphabetical order immediately (matching loadSessions /
365+ // SessionUpserted) so the new session doesn't sit out-of-order at the
366+ // top until the SSE SessionUpserted event arrives to re-sort.
367+ sessions.sortBy { it.title.lowercase() }
361368 }
362369 emit()
363370 // SelectSession takes over the loading state (loads messages).
app/src/main/java/soy/iko/opencode/ui/OpencodeApp.kt+24−1
@@ -32,6 +32,7 @@ import soy.iko.opencode.ui.screens.SessionsScreen
3232fun OpencodeApp(state: UiState, dispatch: (Event) -> Unit) {
3333 val snackbar = remember { SnackbarHostState() }
3434 var showStopConfirm by remember { mutableStateOf(false) }
35+ var showDisconnectConfirm by remember { mutableStateOf(false) }
3536
3637 LaunchedEffect(state.error) {
3738 state.error?.let {
@@ -49,7 +50,8 @@ fun OpencodeApp(state: UiState, dispatch: (Event) -> Unit) {
4950 when (state.screen) {
5051 // Leaving mid-generation would silently drop the reply, so confirm first.
5152 Screen.Chat -> if (state.generating) showStopConfirm = true else dispatch(Event.NavigateToSessions)
52− Screen.Sessions -> dispatch(Event.NavigateToConnect)
53+ // Disconnecting drops the server session, so guard the back button.
54+ Screen.Sessions -> showDisconnectConfirm = true
5355 Screen.Connect -> Unit
5456 }
5557 }
@@ -76,6 +78,27 @@ fun OpencodeApp(state: UiState, dispatch: (Event) -> Unit) {
7678 )
7779 }
7880
81+ if (showDisconnectConfirm) {
82+ AlertDialog(
83+ onDismissRequest = { showDisconnectConfirm = false },
84+ title = { Text("Disconnect?") },
85+ text = { Text("You will be disconnected from the server.") },
86+ confirmButton = {
87+ TextButton(onClick = {
88+ showDisconnectConfirm = false
89+ dispatch(Event.NavigateToConnect)
90+ }) {
91+ Text("Disconnect")
92+ }
93+ },
94+ dismissButton = {
95+ TextButton(onClick = { showDisconnectConfirm = false }) {
96+ Text("Cancel")
97+ }
98+ },
99+ )
100+ }
101+
79102 Scaffold(snackbarHost = { SnackbarHost(snackbar) }) { padding ->
80103 AnimatedContent(
81104 targetState = state.screen,
app/src/main/java/soy/iko/opencode/ui/components/MessageBubble.kt+13−1
@@ -127,12 +127,24 @@ fun MessageBubble(message: MessageView, modifier: Modifier = Modifier, onRetry:
127127
128128 message.reasoning?.let { ReasoningBlock(it, context) }
129129
130+ // Give a quick overview of how many tools ran before listing them.
131+ if (message.tools.size > 1) {
132+ Text(
133+ text = "${message.tools.size} tool calls",
134+ style = MaterialTheme.typography.labelSmall,
135+ color = MaterialTheme.colorScheme.onSurfaceVariant,
136+ modifier = Modifier.padding(bottom = 4.dp),
137+ )
138+ }
139+
130140 message.tools.forEach { ToolRow(it) }
131141
132142 SelectionContainer {
133143 if (message.text.isNotEmpty()) {
134144 val linkColor = MaterialTheme.colorScheme.primary
135− val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
145+ // Tint the code background off the bubble's own foreground colour so it
146+ // contrasts on both bubble types (user bubbles use primaryContainer).
147+ val codeBackground = (if (isUser) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface).copy(alpha = 0.08f)
136148 val annotatedText = remember(message.text, message.streaming, linkColor, codeBackground, context) {
137149 buildLinkedText(message.text, message.streaming, linkColor, codeBackground, context)
138150 }
app/src/main/java/soy/iko/opencode/ui/screens/ConnectScreen.kt+4−1
@@ -170,7 +170,10 @@ fun ConnectScreen(state: UiState, dispatch: (Event) -> Unit) {
170170
171171 Button(
172172 onClick = { dispatch(Event.Connect) },
173− enabled = !state.loading && state.serverUrl.isNotBlank(),
173+ // When auth is required, keep the button disabled until at least one
174+ // credential is entered — otherwise it just triggers a validation error.
175+ enabled = !state.loading && state.serverUrl.isNotBlank() &&
176+ (!state.authRequired || state.username.isNotBlank() || state.password.isNotBlank()),
174177 modifier = Modifier.fillMaxWidth(),
175178 ) {
176179 if (state.loading) {