@@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
26import kotlinx.coroutines.flow.SharingStarted
26import kotlinx.coroutines.flow.SharingStarted
27import kotlinx.coroutines.flow.StateFlow
27import kotlinx.coroutines.flow.StateFlow
28import kotlinx.coroutines.flow.asStateFlow
28import kotlinx.coroutines.flow.asStateFlow
29+import kotlinx.coroutines.flow.catch
29import kotlinx.coroutines.flow.collectLatest
30import kotlinx.coroutines.flow.collectLatest
30import kotlinx.coroutines.flow.debounce
31import kotlinx.coroutines.flow.debounce
31import kotlinx.coroutines.flow.drop
32import kotlinx.coroutines.flow.drop
@@ -55,6 +56,10 @@ class ChatViewModel(
55 conn?.repository?.observeMessages(sessionId) ?: flowOf(emptyList())
56 conn?.repository?.observeMessages(sessionId) ?: flowOf(emptyList())
57 .onEach { _loading.value = false }
58 .onEach { _loading.value = false }
60+ _loading.value = false
61+ _error.value = container.friendlyError(it)
59 scope = viewModelScope,
64 scope = viewModelScope,
60 started = SharingStarted.WhileSubscribed(5_000),
65 started = SharingStarted.WhileSubscribed(5_000),
@@ -73,6 +78,9 @@ class ChatViewModel(
73 private val _modelsLoading = MutableStateFlow(true)
78 private val _modelsLoading = MutableStateFlow(true)
74 val modelsLoading: StateFlow<Boolean> = _modelsLoading.asStateFlow()
79 val modelsLoading: StateFlow<Boolean> = _modelsLoading.asStateFlow()
81+ private val _modelsError = MutableStateFlow(false)
82+ val modelsError: StateFlow<Boolean> = _modelsError.asStateFlow()
76 private val _selectedModel = MutableStateFlow<ModelOption?>(null)
84 private val _selectedModel = MutableStateFlow<ModelOption?>(null)
77 val selectedModel: StateFlow<ModelOption?> = _selectedModel.asStateFlow()
85 val selectedModel: StateFlow<ModelOption?> = _selectedModel.asStateFlow()
@@ -94,6 +102,9 @@ class ChatViewModel(
94 private val _agentsLoading = MutableStateFlow(true)
102 private val _agentsLoading = MutableStateFlow(true)
95 val agentsLoading: StateFlow<Boolean> = _agentsLoading.asStateFlow()
103 val agentsLoading: StateFlow<Boolean> = _agentsLoading.asStateFlow()
105+ private val _agentsError = MutableStateFlow(false)
106+ val agentsError: StateFlow<Boolean> = _agentsError.asStateFlow()
97 private val _selectedAgent = MutableStateFlow<String?>(null)
108 private val _selectedAgent = MutableStateFlow<String?>(null)
98 val selectedAgent: StateFlow<String?> = _selectedAgent.asStateFlow()
109 val selectedAgent: StateFlow<String?> = _selectedAgent.asStateFlow()
@@ -103,6 +114,9 @@ class ChatViewModel(
103 private val _commandsLoading = MutableStateFlow(true)
114 private val _commandsLoading = MutableStateFlow(true)
104 val commandsLoading: StateFlow<Boolean> = _commandsLoading.asStateFlow()
115 val commandsLoading: StateFlow<Boolean> = _commandsLoading.asStateFlow()
117+ private val _commandsError = MutableStateFlow(false)
118+ val commandsError: StateFlow<Boolean> = _commandsError.asStateFlow()
106 private val _sessionTitle = MutableStateFlow<String?>(null)
120 private val _sessionTitle = MutableStateFlow<String?>(null)
107 val sessionTitle: StateFlow<String?> = _sessionTitle.asStateFlow()
121 val sessionTitle: StateFlow<String?> = _sessionTitle.asStateFlow()
@@ -133,6 +147,49 @@ class ChatViewModel(
134 fun selectAgent(name: String?) { _selectedAgent.value = name }
148 fun selectAgent(name: String?) { _selectedAgent.value = name }
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)
163+ _modelsLoading.value = false
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
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
136 fun updateDraft(text: String) {
193 fun updateDraft(text: String) {
137 _draft.value = text
194 _draft.value = text
@@ -181,10 +238,11 @@ class ChatViewModel(
181 // sending with no model just uses the server's default agent/model.
238 // sending with no model just uses the server's default agent/model.
182 viewModelScope.launch {
239 viewModelScope.launch {
183 container.activeConnection.collectLatest { conn ->
240 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 }
185 _modelsLoading.value = true
242 _modelsLoading.value = true
243+ _modelsError.value = false
186 runCatchingCancellable { conn.api.providers() }
244 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 }
188 .getOrNull()?.let { resp ->
246 .getOrNull()?.let { resp ->
189 val options = resp.toOptions()
247 val options = resp.toOptions()
190 _models.value = options
248 _models.value = options
@@ -196,10 +254,11 @@ class ChatViewModel(
196 // Load the agent catalog (non-fatal).
254 // Load the agent catalog (non-fatal).
197 viewModelScope.launch {
255 viewModelScope.launch {
198 container.activeConnection.collectLatest { conn ->
256 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 }
200 _agentsLoading.value = true
258 _agentsLoading.value = true
259+ _agentsError.value = false
201 runCatchingCancellable { conn.api.agents() }
260 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 }
203 .getOrNull()?.let { _agents.value = it }
262 .getOrNull()?.let { _agents.value = it }
204 _agentsLoading.value = false
263 _agentsLoading.value = false
@@ -207,10 +266,11 @@ class ChatViewModel(
207 // Load the command catalog (non-fatal).
266 // Load the command catalog (non-fatal).
208 viewModelScope.launch {
267 viewModelScope.launch {
209 container.activeConnection.collectLatest { conn ->
268 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 }
211 _commandsLoading.value = true
270 _commandsLoading.value = true
271+ _commandsError.value = false
212 runCatchingCancellable { conn.api.commands() }
272 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 }
214 .getOrNull()?.let { _commands.value = it }
274 .getOrNull()?.let { _commands.value = it }
215 _commandsLoading.value = false
275 _commandsLoading.value = false
@@ -264,11 +324,14 @@ class ChatViewModel(
264 // Only restore the draft if the user hasn't typed anything new since.
324 // Only restore the draft if the user hasn't typed anything new since.
265 if (_draft.value.isBlank()) updateDraft(trimmed)
325 if (_draft.value.isBlank()) updateDraft(trimmed)
266 _error.value = container.friendlyError(it)
326 _error.value = container.friendlyError(it)
327+ _running.value = false
267 }.onSuccess {
328 }.onSuccess {
268 // Send succeeded — now it's safe to persist the empty draft.
329 // Send succeeded — now it's safe to persist the empty draft.
269 container.draftStore.set(sessionId, "")
330 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).
271− _running.value = false
273 return true
336 return true
@@ -288,8 +351,10 @@ class ChatViewModel(
288 viewModelScope.launch {
351 viewModelScope.launch {
289 runCatchingCancellable {
352 runCatchingCancellable {
290 conn.repository.runCommand(sessionId, command.name, agent = command.agent)
353 conn.repository.runCommand(sessionId, command.name, agent = command.agent)
291− }.onFailure { _error.value = container.friendlyError(it) }
355+ _error.value = container.friendlyError(it)
292− _running.value = false
356+ _running.value = false
@@ -307,7 +372,7 @@ class ChatViewModel(
307 if (container.activeConnection.value != null) return
372 if (container.activeConnection.value != null) return
308 viewModelScope.launch {
373 viewModelScope.launch {
309 _loading.value = true
374 _loading.value = true
310− val recent = runCatching { container.profileStore.profiles.first() }
375+ val recent = runCatchingCancellable { container.profileStore.profiles.first() }
311 .getOrDefault(emptyList())
376 .getOrDefault(emptyList())
312 .firstOrNull() ?: run { _loading.value = false; return@launch }
377 .firstOrNull() ?: run { _loading.value = false; return@launch }
313 runCatchingCancellable {
378 runCatchingCancellable {