@@ -27,6 +27,7 @@ import androidx.compose.foundation.text.KeyboardOptions
27import androidx.compose.material.icons.Icons
27import androidx.compose.material.icons.Icons
28import androidx.compose.material.icons.automirrored.filled.ArrowBack
28import androidx.compose.material.icons.automirrored.filled.ArrowBack
29import androidx.compose.material.icons.automirrored.filled.Send
29import androidx.compose.material.icons.automirrored.filled.Send
30+import androidx.compose.material.icons.filled.Close
30import androidx.compose.material.icons.filled.KeyboardArrowDown
31import androidx.compose.material.icons.filled.KeyboardArrowDown
31import androidx.compose.material.icons.filled.Stop
32import androidx.compose.material.icons.filled.Stop
32import androidx.compose.material3.CircularProgressIndicator
33import androidx.compose.material3.CircularProgressIndicator
@@ -136,6 +137,7 @@ fun ChatScreen(state: UiState, dispatch: (Event) -> Unit) {
136 ChatInputBar(
137 ChatInputBar(
137 draft = state.draft,
138 draft = state.draft,
138 generating = state.generating,
139 generating = state.generating,
140+ loading = state.loading && state.messages.isEmpty(),
139 onDraftChange = { dispatch(Event.DraftChanged(it)) },
141 onDraftChange = { dispatch(Event.DraftChanged(it)) },
140 onSend = { dispatch(Event.SendMessage(state.draft)) },
142 onSend = { dispatch(Event.SendMessage(state.draft)) },
141 onStop = { dispatch(Event.CancelGeneration) },
143 onStop = { dispatch(Event.CancelGeneration) },
@@ -149,6 +151,12 @@ fun ChatScreen(state: UiState, dispatch: (Event) -> Unit) {
149 if (state.loading && state.messages.isEmpty()) {
151 if (state.loading && state.messages.isEmpty()) {
150 LinearProgressIndicator(Modifier.fillMaxWidth())
152 LinearProgressIndicator(Modifier.fillMaxWidth())
154+ if (state.error != null && state.messages.isNotEmpty()) {
156+ message = state.error,
157+ onDismiss = { dispatch(Event.DismissError) },
152 Box(Modifier.fillMaxWidth().weight(1f)) {
160 Box(Modifier.fillMaxWidth().weight(1f)) {
153 if (state.messages.isEmpty() && !state.loading) {
161 if (state.messages.isEmpty() && !state.loading) {
@@ -242,10 +250,40 @@ private fun ReconnectingBanner() {
254+private fun ErrorBanner(message: String, onDismiss: () -> Unit) {
256+ modifier = Modifier.fillMaxWidth(),
257+ color = MaterialTheme.colorScheme.errorContainer,
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),
266+ style = MaterialTheme.typography.bodySmall,
267+ color = MaterialTheme.colorScheme.onErrorContainer,
268+ modifier = Modifier.weight(1f),
270+ IconButton(onClick = onDismiss, modifier = Modifier.size(32.dp)) {
273+ contentDescription = "Dismiss error",
274+ tint = MaterialTheme.colorScheme.onErrorContainer,
275+ modifier = Modifier.size(18.dp),
245@Composable
282@Composable
246private fun ChatInputBar(
283private fun ChatInputBar(
247 draft: String,
284 draft: String,
248 generating: Boolean,
285 generating: Boolean,
249 onDraftChange: (String) -> Unit,
287 onDraftChange: (String) -> Unit,
250 onSend: () -> Unit,
288 onSend: () -> Unit,
251 onStop: () -> Unit,
289 onStop: () -> Unit,
@@ -261,11 +299,12 @@ private fun ChatInputBar(
261 value = draft,
299 value = draft,
262 onValueChange = onDraftChange,
300 onValueChange = onDraftChange,
263 modifier = Modifier.weight(1f),
301 modifier = Modifier.weight(1f),
264 placeholder = { Text("Message opencode…") },
303 placeholder = { Text("Message opencode…") },
265 maxLines = 5,
304 maxLines = 5,
266 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
305 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
267 keyboardActions = KeyboardActions(onSend = {
306 keyboardActions = KeyboardActions(onSend = {
268− if (!generating && draft.isNotBlank()) onSend()
307+ if (!generating && !loading && draft.isNotBlank()) onSend()
271 if (generating) {
310 if (generating) {
@@ -278,7 +317,7 @@ private fun ChatInputBar(
278 haptic.performHapticFeedback(HapticFeedbackType.LongPress)
317 haptic.performHapticFeedback(HapticFeedbackType.LongPress)
281− enabled = draft.isNotBlank(),
320+ enabled = draft.isNotBlank() && !loading,
283 Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send")
322 Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send")