@@ -60,6 +60,9 @@ class OpencodeCore(
60 // Tracks the in-flight loadMessages coroutine. Rapidly switching sessions launches
60 // Tracks the in-flight loadMessages coroutine. Rapidly switching sessions launches
61 // several; cancel the prior one so only the newest fetch runs (saves needless requests).
61 // several; cancel the prior one so only the newest fetch runs (saves needless requests).
62 private var loadMessagesJob: Job? = null
62 private var loadMessagesJob: Job? = null
63+ // Tracks the in-flight probeHealth coroutine. Rapid double-taps on Connect would
64+ // otherwise launch two concurrent health probes; cancel the prior one first.
65+ private var connectJob: Job? = null
63 // Message IDs removed via SSE; a late MessageUpdated for one must not resurrect it.
66 // Message IDs removed via SSE; a late MessageUpdated for one must not resurrect it.
64 private val removedMessageIds = mutableSetOf<String>()
67 private val removedMessageIds = mutableSetOf<String>()
@@ -84,7 +87,8 @@ class OpencodeCore(
84 // Leave authRequired as-is; probeHealth sets it from the response (true on
87 // Leave authRequired as-is; probeHealth sets it from the response (true on
85 // 401, false on success). Resetting it here just flickers the auth fields.
88 // 401, false on success). Resetting it here just flickers the auth fields.
86 loading = true; error = null; emit()
89 loading = true; error = null; emit()
87− scope.launch { probeHealth() }
91+ connectJob = scope.launch { probeHealth() }
90 Event.LoadSessions -> {
94 Event.LoadSessions -> {
@@ -208,6 +212,9 @@ class OpencodeCore(
208 val text = raw.trim()
212 val text = raw.trim()
209 val session = currentSessionId ?: return
213 val session = currentSessionId ?: return
210 if (text.isEmpty()) return
214 if (text.isEmpty()) return
215+ // Debounce rapid double-taps on send: a send is already in flight, so a second
216+ // tap would create a duplicate user message and fire a duplicate request.
217+ if (generating) return
211 localSeq += 1
218 localSeq += 1
212 val user = MsgState("local-$localSeq", "user", System.currentTimeMillis()).apply {
219 val user = MsgState("local-$localSeq", "user", System.currentTimeMillis()).apply {
213 status = MessageStatus.Pending
220 status = MessageStatus.Pending
@@ -422,7 +429,7 @@ class OpencodeCore(
423 messages.add(MsgState(info.id, "assistant", info.time.created).apply { this.streaming = streaming })
430 messages.add(MsgState(info.id, "assistant", info.time.created).apply { this.streaming = streaming })
425− info.error?.let { error = it.message(); generating = false }
432+ info.error?.let { error = it.message(); messages.forEach { m -> m.streaming = false }; generating = false }
426 return true
433 return true
@@ -485,6 +492,9 @@ class OpencodeCore(
486 sessions.add(0, SessionView(event.info.id, title))
493 sessions.add(0, SessionView(event.info.id, title))
495+ // Keep the list alphabetically sorted (matching loadSessions) so an
496+ // upserted session doesn't jump to the top and break the ordering.
497+ sessions.sortBy { it.title.lowercase() }
488 if (isCurrent(event.info.id)) currentSessionTitle = title
498 if (isCurrent(event.info.id)) currentSessionTitle = title
489 return true
499 return true