@@ -19,6 +19,7 @@ import soy.iko.opencode.data.network.EventStreamClient
19import soy.iko.opencode.data.repo.SessionRepository
19import soy.iko.opencode.data.repo.SessionRepository
20import soy.iko.opencode.di.AppContainer
20import soy.iko.opencode.di.AppContainer
21import soy.iko.opencode.R
21import soy.iko.opencode.R
22+import soy.iko.opencode.util.runCatchingCancellable
22import android.util.Log
23import android.util.Log
23import kotlinx.coroutines.ExperimentalCoroutinesApi
24import kotlinx.coroutines.ExperimentalCoroutinesApi
24import kotlinx.coroutines.flow.MutableStateFlow
25import kotlinx.coroutines.flow.MutableStateFlow
@@ -155,7 +156,7 @@ class ChatViewModel(
155 container.activeConnection.collectLatest { conn ->
156 container.activeConnection.collectLatest { conn ->
156 if (conn == null) { _models.value = emptyList(); _selectedModel.value = null; _modelsLoading.value = false; return@collectLatest }
157 if (conn == null) { _models.value = emptyList(); _selectedModel.value = null; _modelsLoading.value = false; return@collectLatest }
157 _modelsLoading.value = true
158 _modelsLoading.value = true
158− runCatching { conn.api.providers() }
159+ runCatchingCancellable { conn.api.providers() }
159 .onFailure { Log.w("ChatViewModel", "Failed to load model catalog", it) }
160 .onFailure { Log.w("ChatViewModel", "Failed to load model catalog", it) }
160 .getOrNull()?.let { resp ->
161 .getOrNull()?.let { resp ->
161 val options = resp.toOptions()
162 val options = resp.toOptions()
@@ -170,7 +171,7 @@ class ChatViewModel(
170 container.activeConnection.collectLatest { conn ->
171 container.activeConnection.collectLatest { conn ->
171 if (conn == null) { _agents.value = emptyList(); _agentsLoading.value = false; return@collectLatest }
172 if (conn == null) { _agents.value = emptyList(); _agentsLoading.value = false; return@collectLatest }
172 _agentsLoading.value = true
173 _agentsLoading.value = true
173− runCatching { conn.api.agents() }
174+ runCatchingCancellable { conn.api.agents() }
174 .onFailure { Log.w("ChatViewModel", "Failed to load agent catalog", it) }
175 .onFailure { Log.w("ChatViewModel", "Failed to load agent catalog", it) }
175 .getOrNull()?.let { _agents.value = it }
176 .getOrNull()?.let { _agents.value = it }
176 _agentsLoading.value = false
177 _agentsLoading.value = false
@@ -181,7 +182,7 @@ class ChatViewModel(
181 container.activeConnection.collectLatest { conn ->
182 container.activeConnection.collectLatest { conn ->
182 if (conn == null) { _commands.value = emptyList(); _commandsLoading.value = false; return@collectLatest }
183 if (conn == null) { _commands.value = emptyList(); _commandsLoading.value = false; return@collectLatest }
183 _commandsLoading.value = true
184 _commandsLoading.value = true
184− runCatching { conn.api.commands() }
185+ runCatchingCancellable { conn.api.commands() }
185 .onFailure { Log.w("ChatViewModel", "Failed to load command catalog", it) }
186 .onFailure { Log.w("ChatViewModel", "Failed to load command catalog", it) }
186 .getOrNull()?.let { _commands.value = it }
187 .getOrNull()?.let { _commands.value = it }
187 _commandsLoading.value = false
188 _commandsLoading.value = false
@@ -191,7 +192,7 @@ class ChatViewModel(
191 viewModelScope.launch {
192 viewModelScope.launch {
192 container.activeConnection.collectLatest { conn ->
193 container.activeConnection.collectLatest { conn ->
193 if (conn == null) return@collectLatest
194 if (conn == null) return@collectLatest
194− runCatching { conn.repository.listSessions() }
195+ runCatchingCancellable { conn.repository.listSessions() }
195 .getOrNull()
196 .getOrNull()
196 ?.firstOrNull { it.id == sessionId }
197 ?.firstOrNull { it.id == sessionId }
197 ?.let { _sessionTitle.value = it.displayTitle }
198 ?.let { _sessionTitle.value = it.displayTitle }
@@ -223,7 +224,7 @@ class ChatViewModel(
223 // would be lost forever. The persisted draft is cleared only on success.
224 // would be lost forever. The persisted draft is cleared only on success.
224 _draft.value = ""
225 _draft.value = ""
225 viewModelScope.launch {
226 viewModelScope.launch {
226− runCatching {
227+ runCatchingCancellable {
227 conn.repository.sendPrompt(
228 conn.repository.sendPrompt(
228 sessionId,
229 sessionId,
@@ -257,7 +258,7 @@ class ChatViewModel(
257 if (!_running.compareAndSet(false, true)) return
258 if (!_running.compareAndSet(false, true)) return
258 _error.value = null
259 _error.value = null
259 viewModelScope.launch {
260 viewModelScope.launch {
260− runCatching {
261+ runCatchingCancellable {
261 conn.repository.runCommand(sessionId, command.name, agent = command.agent)
262 conn.repository.runCommand(sessionId, command.name, agent = command.agent)
262 }.onFailure { _error.value = container.friendlyError(it) }
263 }.onFailure { _error.value = container.friendlyError(it) }
263 _running.value = false
264 _running.value = false
@@ -267,9 +268,9 @@ class ChatViewModel(
267 fun abort() {
268 fun abort() {
268 val conn = connection ?: return
269 val conn = connection ?: return
269 viewModelScope.launch {
270 viewModelScope.launch {
270− runCatching { conn.repository.abort(sessionId) }
271+ runCatchingCancellable { conn.repository.abort(sessionId) }
272+ .onSuccess { _running.value = false }
271 .onFailure { _error.value = container.friendlyError(it) }
273 .onFailure { _error.value = container.friendlyError(it) }
272− _running.value = false
@@ -281,7 +282,7 @@ class ChatViewModel(
281 val recent = runCatching { container.profileStore.profiles.first() }
282 val recent = runCatching { container.profileStore.profiles.first() }
282 .getOrDefault(emptyList())
283 .getOrDefault(emptyList())
283 .firstOrNull() ?: run { _loading.value = false; return@launch }
284 .firstOrNull() ?: run { _loading.value = false; return@launch }
284− runCatching {
285+ runCatchingCancellable {
285 val conn = container.connect(recent)
286 val conn = container.connect(recent)
286 conn.api.ping()
287 conn.api.ping()
287 }.onSuccess {
288 }.onSuccess {
@@ -301,10 +302,11 @@ class ChatViewModel(
301 // with a dismissed dialog and a paused tool run.
302 // with a dismissed dialog and a paused tool run.
302 _pendingPermission.value = null
303 _pendingPermission.value = null
303 viewModelScope.launch {
304 viewModelScope.launch {
304− runCatching { conn.api.respondPermission(sessionId, permission.id, response) }
305+ runCatchingCancellable { conn.api.respondPermission(sessionId, permission.id, response) }
305 .onFailure {
306 .onFailure {
306 _error.value = container.friendlyError(it)
307 _error.value = container.friendlyError(it)
308+ // Only restore if no new permission has arrived in the meantime.
307− _pendingPermission.value = permission
309+ if (_pendingPermission.value == null) _pendingPermission.value = permission