@@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
2626import kotlinx.coroutines.flow.SharingStarted
2727import kotlinx.coroutines.flow.StateFlow
2828import kotlinx.coroutines.flow.asStateFlow
29+import kotlinx.coroutines.flow.catch
2930import kotlinx.coroutines.flow.collectLatest
3031import kotlinx.coroutines.flow.debounce
3132import kotlinx.coroutines.flow.drop
@@ -55,6 +56,10 @@ class ChatViewModel(
5556 conn?.repository?.observeMessages(sessionId) ?: flowOf(emptyList())
5657 }
5758 .onEach { _loading.value = false }
59+ .catch {
60+ _loading.value = false
61+ _error.value = container.friendlyError(it)
62+ }
5863 .stateIn(
5964 scope = viewModelScope,
6065 started = SharingStarted.WhileSubscribed(5_000),
@@ -73,6 +78,9 @@ class ChatViewModel(
7378 private val _modelsLoading = MutableStateFlow(true)
7479 val modelsLoading: StateFlow<Boolean> = _modelsLoading.asStateFlow()
7580
81+ private val _modelsError = MutableStateFlow(false)
82+ val modelsError: StateFlow<Boolean> = _modelsError.asStateFlow()
83+
7684 private val _selectedModel = MutableStateFlow<ModelOption?>(null)
7785 val selectedModel: StateFlow<ModelOption?> = _selectedModel.asStateFlow()
7886
@@ -94,6 +102,9 @@ class ChatViewModel(
94102 private val _agentsLoading = MutableStateFlow(true)
95103 val agentsLoading: StateFlow<Boolean> = _agentsLoading.asStateFlow()
96104
105+ private val _agentsError = MutableStateFlow(false)
106+ val agentsError: StateFlow<Boolean> = _agentsError.asStateFlow()
107+
97108 private val _selectedAgent = MutableStateFlow<String?>(null)
98109 val selectedAgent: StateFlow<String?> = _selectedAgent.asStateFlow()
99110
@@ -103,6 +114,9 @@ class ChatViewModel(
103114 private val _commandsLoading = MutableStateFlow(true)
104115 val commandsLoading: StateFlow<Boolean> = _commandsLoading.asStateFlow()
105116
117+ private val _commandsError = MutableStateFlow(false)
118+ val commandsError: StateFlow<Boolean> = _commandsError.asStateFlow()
119+
106120 private val _sessionTitle = MutableStateFlow<String?>(null)
107121 val sessionTitle: StateFlow<String?> = _sessionTitle.asStateFlow()
108122
@@ -133,6 +147,49 @@ class ChatViewModel(
133147
134148 fun selectAgent(name: String?) { _selectedAgent.value = name }
135149
150+ fun reloadModels() {
151+ val conn = connection ?: return
152+ viewModelScope.launch {
153+ conn.api.invalidateCache()
154+ _modelsLoading.value = true
155+ _modelsError.value = false
156+ runCatchingCancellable { conn.api.providers() }
157+ .onFailure { Log.w("ChatViewModel", "Failed to reload model catalog", it); _modelsError.value = true }
158+ .getOrNull()?.let { resp ->
159+ val options = resp.toOptions()
160+ _models.value = options
161+ _selectedModel.value = resp.defaultOption(options)
162+ }
163+ _modelsLoading.value = false
164+ }
165+ }
166+
167+ fun reloadAgents() {
168+ val conn = connection ?: return
169+ viewModelScope.launch {
170+ conn.api.invalidateCache()
171+ _agentsLoading.value = true
172+ _agentsError.value = false
173+ runCatchingCancellable { conn.api.agents() }
174+ .onFailure { Log.w("ChatViewModel", "Failed to reload agent catalog", it); _agentsError.value = true }
175+ .getOrNull()?.let { _agents.value = it }
176+ _agentsLoading.value = false
177+ }
178+ }
179+
180+ fun reloadCommands() {
181+ val conn = connection ?: return
182+ viewModelScope.launch {
183+ conn.api.invalidateCache()
184+ _commandsLoading.value = true
185+ _commandsError.value = false
186+ runCatchingCancellable { conn.api.commands() }
187+ .onFailure { Log.w("ChatViewModel", "Failed to reload command catalog", it); _commandsError.value = true }
188+ .getOrNull()?.let { _commands.value = it }
189+ _commandsLoading.value = false
190+ }
191+ }
192+
136193 fun updateDraft(text: String) {
137194 _draft.value = text
138195 }
@@ -181,10 +238,11 @@ class ChatViewModel(
181238 // sending with no model just uses the server's default agent/model.
182239 viewModelScope.launch {
183240 container.activeConnection.collectLatest { conn ->
184− if (conn == null) { _models.value = emptyList(); _selectedModel.value = null; _modelsLoading.value = false; return@collectLatest }
241+ if (conn == null) { _models.value = emptyList(); _selectedModel.value = null; _modelsLoading.value = false; _modelsError.value = false; return@collectLatest }
185242 _modelsLoading.value = true
243+ _modelsError.value = false
186244 runCatchingCancellable { conn.api.providers() }
187− .onFailure { Log.w("ChatViewModel", "Failed to load model catalog", it) }
245+ .onFailure { Log.w("ChatViewModel", "Failed to load model catalog", it); _modelsError.value = true }
188246 .getOrNull()?.let { resp ->
189247 val options = resp.toOptions()
190248 _models.value = options
@@ -196,10 +254,11 @@ class ChatViewModel(
196254 // Load the agent catalog (non-fatal).
197255 viewModelScope.launch {
198256 container.activeConnection.collectLatest { conn ->
199− if (conn == null) { _agents.value = emptyList(); _agentsLoading.value = false; return@collectLatest }
257+ if (conn == null) { _agents.value = emptyList(); _agentsLoading.value = false; _agentsError.value = false; return@collectLatest }
200258 _agentsLoading.value = true
259+ _agentsError.value = false
201260 runCatchingCancellable { conn.api.agents() }
202− .onFailure { Log.w("ChatViewModel", "Failed to load agent catalog", it) }
261+ .onFailure { Log.w("ChatViewModel", "Failed to load agent catalog", it); _agentsError.value = true }
203262 .getOrNull()?.let { _agents.value = it }
204263 _agentsLoading.value = false
205264 }
@@ -207,10 +266,11 @@ class ChatViewModel(
207266 // Load the command catalog (non-fatal).
208267 viewModelScope.launch {
209268 container.activeConnection.collectLatest { conn ->
210− if (conn == null) { _commands.value = emptyList(); _commandsLoading.value = false; return@collectLatest }
269+ if (conn == null) { _commands.value = emptyList(); _commandsLoading.value = false; _commandsError.value = false; return@collectLatest }
211270 _commandsLoading.value = true
271+ _commandsError.value = false
212272 runCatchingCancellable { conn.api.commands() }
213− .onFailure { Log.w("ChatViewModel", "Failed to load command catalog", it) }
273+ .onFailure { Log.w("ChatViewModel", "Failed to load command catalog", it); _commandsError.value = true }
214274 .getOrNull()?.let { _commands.value = it }
215275 _commandsLoading.value = false
216276 }
@@ -264,11 +324,14 @@ class ChatViewModel(
264324 // Only restore the draft if the user hasn't typed anything new since.
265325 if (_draft.value.isBlank()) updateDraft(trimmed)
266326 _error.value = container.friendlyError(it)
327+ _running.value = false
267328 }.onSuccess {
268329 // Send succeeded — now it's safe to persist the empty draft.
269330 container.draftStore.set(sessionId, "")
331+ // Don't reset _running here: the agent continues streaming via SSE.
332+ // _running is cleared on SessionIdle/SessionError (see event collector)
333+ // or when the SSE stream drops (see connection state watcher below).
270334 }
271− _running.value = false
272335 }
273336 return true
274337 }
@@ -288,8 +351,10 @@ class ChatViewModel(
288351 viewModelScope.launch {
289352 runCatchingCancellable {
290353 conn.repository.runCommand(sessionId, command.name, agent = command.agent)
291− }.onFailure { _error.value = container.friendlyError(it) }
292− _running.value = false
354+ }.onFailure {
355+ _error.value = container.friendlyError(it)
356+ _running.value = false
357+ }
293358 }
294359 }
295360
@@ -307,7 +372,7 @@ class ChatViewModel(
307372 if (container.activeConnection.value != null) return
308373 viewModelScope.launch {
309374 _loading.value = true
310− val recent = runCatching { container.profileStore.profiles.first() }
375+ val recent = runCatchingCancellable { container.profileStore.profiles.first() }
311376 .getOrDefault(emptyList())
312377 .firstOrNull() ?: run { _loading.value = false; return@launch }
313378 runCatchingCancellable {