@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
2424import kotlinx.coroutines.flow.SharingStarted
2525import kotlinx.coroutines.flow.StateFlow
2626import kotlinx.coroutines.flow.asStateFlow
27+import kotlinx.coroutines.flow.collectLatest
2728import kotlinx.coroutines.flow.first
2829import kotlinx.coroutines.flow.flatMapLatest
2930import kotlinx.coroutines.flow.flowOf
@@ -114,8 +115,8 @@ class ChatViewModel(
114115 init {
115116 // Watch the bus for run completion / errors / permission asks for this session.
116117 viewModelScope.launch {
117− container.activeConnection.collect { conn ->
118− if (conn == null) return@collect
118+ container.activeConnection.collectLatest { conn ->
119+ if (conn == null) return@collectLatest
119120 conn.events.events.collect { event ->
120121 if (SessionRepository.isIdle(event, sessionId)) _running.value = false
121122 if (SessionRepository.isError(event, sessionId)) {
@@ -141,8 +142,8 @@ class ChatViewModel(
141142 // Load the model catalog; preselect the server default. Failure is non-fatal —
142143 // sending with no model just uses the server's default agent/model.
143144 viewModelScope.launch {
144− container.activeConnection.collect { conn ->
145− if (conn == null) return@collect
145+ container.activeConnection.collectLatest { conn ->
146+ if (conn == null) return@collectLatest
146147 runCatching { conn.api.providers() }.getOrNull()?.let { resp ->
147148 val options = resp.toOptions()
148149 _models.value = options
@@ -152,22 +153,22 @@ class ChatViewModel(
152153 }
153154 // Load the agent catalog (non-fatal).
154155 viewModelScope.launch {
155− container.activeConnection.collect { conn ->
156− if (conn == null) return@collect
156+ container.activeConnection.collectLatest { conn ->
157+ if (conn == null) return@collectLatest
157158 runCatching { conn.api.agents() }.getOrNull()?.let { _agents.value = it }
158159 }
159160 }
160161 // Load the command catalog (non-fatal).
161162 viewModelScope.launch {
162− container.activeConnection.collect { conn ->
163− if (conn == null) return@collect
163+ container.activeConnection.collectLatest { conn ->
164+ if (conn == null) return@collectLatest
164165 runCatching { conn.api.commands() }.getOrNull()?.let { _commands.value = it }
165166 }
166167 }
167168 // Resolve the human-readable session title for the app bar (non-fatal).
168169 viewModelScope.launch {
169− container.activeConnection.collect { conn ->
170− if (conn == null) return@collect
170+ container.activeConnection.collectLatest { conn ->
171+ if (conn == null) return@collectLatest
171172 runCatching { conn.repository.listSessions() }
172173 .getOrNull()
173174 ?.firstOrNull { it.id == sessionId }
@@ -177,8 +178,8 @@ class ChatViewModel(
177178 // If the SSE stream drops mid-run, the run indicator would spin forever;
178179 // reset it so the UI doesn't look stuck while the banner shows "Reconnecting…".
179180 viewModelScope.launch {
180− container.activeConnection.collect { conn ->
181− if (conn == null) return@collect
181+ container.activeConnection.collectLatest { conn ->
182+ if (conn == null) return@collectLatest
182183 conn.events.state.collect { state ->
183184 if (state == EventStreamClient.ConnectionState.Disconnected && _running.value) {
184185 _running.value = false
@@ -196,7 +197,10 @@ class ChatViewModel(
196197 _running.value = true
197198 _error.value = null
198199 _failedDraft.value = null
199− updateDraft("")
200+ // Clear the in-memory draft for the UI immediately, but don't persist the clear
201+ // yet — if the send fails and the process dies before we restore, the draft
202+ // would be lost forever. The persisted draft is cleared only on success.
203+ _draft.value = ""
200204 viewModelScope.launch {
201205 val ok = runCatching {
202206 conn.repository.sendPrompt(
@@ -212,6 +216,9 @@ class ChatViewModel(
212216 // Only restore the draft if the user hasn't typed anything new since.
213217 if (_draft.value.isBlank()) updateDraft(trimmed)
214218 _error.value = container.string(R.string.error_failed_to_send)
219+ } else {
220+ // Send succeeded — now it's safe to persist the empty draft.
221+ container.draftStore.set(sessionId, "")
215222 }
216223 }
217224 return true
@@ -267,11 +274,16 @@ class ChatViewModel(
267274
268275 fun respondPermission(permission: Permission, response: PermissionResponse) {
269276 val conn = connection ?: return
270− // Clear optimistically; permission.replied will confirm.
277+ // Clear optimistically; permission.replied will confirm. If the call fails we
278+ // restore the pending permission so the user can retry instead of being stuck
279+ // with a dismissed dialog and a paused tool run.
271280 _pendingPermission.value = null
272281 viewModelScope.launch {
273282 runCatching { conn.api.respondPermission(sessionId, permission.id, response) }
274− .onFailure { _error.value = container.friendlyError(it) }
283+ .onFailure {
284+ _error.value = container.friendlyError(it)
285+ _pendingPermission.value = permission
286+ }
275287 }
276288 }
277289}