@@ -27,6 +27,7 @@ import androidx.compose.foundation.text.KeyboardOptions
2727import androidx.compose.material.icons.Icons
2828import androidx.compose.material.icons.automirrored.filled.ArrowBack
2929import androidx.compose.material.icons.automirrored.filled.Send
30+import androidx.compose.material.icons.filled.Close
3031import androidx.compose.material.icons.filled.KeyboardArrowDown
3132import androidx.compose.material.icons.filled.Stop
3233import androidx.compose.material3.CircularProgressIndicator
@@ -136,6 +137,7 @@ fun ChatScreen(state: UiState, dispatch: (Event) -> Unit) {
136137 ChatInputBar(
137138 draft = state.draft,
138139 generating = state.generating,
140+ loading = state.loading && state.messages.isEmpty(),
139141 onDraftChange = { dispatch(Event.DraftChanged(it)) },
140142 onSend = { dispatch(Event.SendMessage(state.draft)) },
141143 onStop = { dispatch(Event.CancelGeneration) },
@@ -149,6 +151,12 @@ fun ChatScreen(state: UiState, dispatch: (Event) -> Unit) {
149151 if (state.loading && state.messages.isEmpty()) {
150152 LinearProgressIndicator(Modifier.fillMaxWidth())
151153 }
154+ if (state.error != null && state.messages.isNotEmpty()) {
155+ ErrorBanner(
156+ message = state.error,
157+ onDismiss = { dispatch(Event.DismissError) },
158+ )
159+ }
152160 Box(Modifier.fillMaxWidth().weight(1f)) {
153161 if (state.messages.isEmpty() && !state.loading) {
154162 Text(
@@ -242,10 +250,40 @@ private fun ReconnectingBanner() {
242250 }
243251}
244252
253+@Composable
254+private fun ErrorBanner(message: String, onDismiss: () -> Unit) {
255+ Surface(
256+ modifier = Modifier.fillMaxWidth(),
257+ color = MaterialTheme.colorScheme.errorContainer,
258+ ) {
259+ Row(
260+ modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 6.dp, bottom = 6.dp, end = 4.dp),
261+ verticalAlignment = Alignment.CenterVertically,
262+ horizontalArrangement = Arrangement.spacedBy(8.dp),
263+ ) {
264+ Text(
265+ text = message,
266+ style = MaterialTheme.typography.bodySmall,
267+ color = MaterialTheme.colorScheme.onErrorContainer,
268+ modifier = Modifier.weight(1f),
269+ )
270+ IconButton(onClick = onDismiss, modifier = Modifier.size(32.dp)) {
271+ Icon(
272+ Icons.Filled.Close,
273+ contentDescription = "Dismiss error",
274+ tint = MaterialTheme.colorScheme.onErrorContainer,
275+ modifier = Modifier.size(18.dp),
276+ )
277+ }
278+ }
279+ }
280+}
281+
245282@Composable
246283private fun ChatInputBar(
247284 draft: String,
248285 generating: Boolean,
286+ loading: Boolean,
249287 onDraftChange: (String) -> Unit,
250288 onSend: () -> Unit,
251289 onStop: () -> Unit,
@@ -261,11 +299,12 @@ private fun ChatInputBar(
261299 value = draft,
262300 onValueChange = onDraftChange,
263301 modifier = Modifier.weight(1f),
302+ enabled = !loading,
264303 placeholder = { Text("Message opencode…") },
265304 maxLines = 5,
266305 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
267306 keyboardActions = KeyboardActions(onSend = {
268− if (!generating && draft.isNotBlank()) onSend()
307+ if (!generating && !loading && draft.isNotBlank()) onSend()
269308 }),
270309 )
271310 if (generating) {
@@ -278,7 +317,7 @@ private fun ChatInputBar(
278317 haptic.performHapticFeedback(HapticFeedbackType.LongPress)
279318 onSend()
280319 },
281− enabled = draft.isNotBlank(),
320+ enabled = draft.isNotBlank() && !loading,
282321 ) {
283322 Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send")
284323 }