@@ -65,7 +65,9 @@ class OpencodeCore(
65 // The auth fields are showing but empty — sending the request now
65 // The auth fields are showing but empty — sending the request now
66 // would just 401 again in a loop. Prompt for credentials instead.
66 // would just 401 again in a loop. Prompt for credentials instead.
67 if (authRequired && !hasCredentials()) { error = "Enter username and password"; emit(); return }
67 if (authRequired && !hasCredentials()) { error = "Enter username and password"; emit(); return }
68+ // Leave authRequired as-is; probeHealth sets it from the response (true on
69+ // 401, false on success). Resetting it here just flickers the auth fields.
68− loading = true; error = null; authRequired = false; emit()
70+ loading = true; error = null; emit()
69 scope.launch { probeHealth() }
71 scope.launch { probeHealth() }
@@ -335,15 +337,17 @@ class OpencodeCore(
336 private fun handleServerEvent(json: String) {
338 private fun handleServerEvent(json: String) {
337 val event = Protocol.parseEvent(json) ?: return
339 val event = Protocol.parseEvent(json) ?: return
338− applyEvent(event)
340+ // Only re-render if the event actually changed our state; applyEvent returns
339− emit()
341+ // false for events that don't apply (other session, unknown message, …).
342+ if (applyEvent(event)) emit()
345+ /** Apply a server event to the model. Returns true iff any state actually changed. */
342− private fun applyEvent(event: Protocol.Event) {
346+ private fun applyEvent(event: Protocol.Event): Boolean {
343 when (event) {
347 when (event) {
344 is Protocol.Event.MessageUpdated -> {
348 is Protocol.Event.MessageUpdated -> {
345 val info = event.info
349 val info = event.info
346− if (info.role != "assistant" || !isCurrent(info.sessionId)) return
350+ if (info.role != "assistant" || !isCurrent(info.sessionId)) return false
347 val streaming = info.time.completed == null && info.error == null
351 val streaming = info.time.completed == null && info.error == null
348 val existing = messages.firstOrNull { it.id == info.id }
352 val existing = messages.firstOrNull { it.id == info.id }
349 if (existing != null) {
353 if (existing != null) {
@@ -353,11 +357,12 @@ class OpencodeCore(
353 messages.add(MsgState(info.id, "assistant", info.time.created).apply { this.streaming = streaming })
357 messages.add(MsgState(info.id, "assistant", info.time.created).apply { this.streaming = streaming })
355 info.error?.let { error = it.message(); generating = false }
359 info.error?.let { error = it.message(); generating = false }
358 is Protocol.Event.PartUpdated -> {
363 is Protocol.Event.PartUpdated -> {
359 val part = event.part
364 val part = event.part
360− val msg = messages.firstOrNull { it.id == part.messageId } ?: return
365+ val msg = messages.firstOrNull { it.id == part.messageId } ?: return false
361 when (part) {
366 when (part) {
362 is Protocol.Part.Text -> if (!part.synthetic) {
367 is Protocol.Part.Text -> if (!part.synthetic) {
363 msg.textParts[part.id] = pickText(part.text, event.delta, msg.textParts[part.id])
368 msg.textParts[part.id] = pickText(part.text, event.delta, msg.textParts[part.id])
@@ -367,24 +372,31 @@ class OpencodeCore(
367 is Protocol.Part.Tool ->
372 is Protocol.Part.Tool ->
368 msg.toolParts[part.id] = ToolView(part.tool, part.status, part.title)
373 msg.toolParts[part.id] = ToolView(part.tool, part.status, part.title)
378+ is Protocol.Event.PartRemoved -> {
372− is Protocol.Event.PartRemoved -> messages.firstOrNull { it.id == event.messageId }?.remove(event.partId)
379+ val msg = messages.firstOrNull { it.id == event.messageId } ?: return false
380+ msg.remove(event.partId)
374− is Protocol.Event.MessageRemoved -> messages.removeAll { it.id == event.messageId }
384+ is Protocol.Event.MessageRemoved -> return messages.removeAll { it.id == event.messageId }
376− is Protocol.Event.SessionIdle -> if (isCurrent(event.sessionId)) {
386+ is Protocol.Event.SessionIdle -> {
387+ if (!isCurrent(event.sessionId)) return false
377 generating = false
388 generating = false
378 messages.forEach { it.streaming = false }
389 messages.forEach { it.streaming = false }
381 is Protocol.Event.SessionError -> {
393 is Protocol.Event.SessionError -> {
382 val scoped = event.sessionId == null || isCurrent(event.sessionId)
394 val scoped = event.sessionId == null || isCurrent(event.sessionId)
383− if (scoped) {
395+ if (!scoped) return false
384− event.error?.let { error = it.message() }
396+ event.error?.let { error = it.message() }
385− generating = false
397+ generating = false
386− messages.forEach { it.streaming = false }
398+ messages.forEach { it.streaming = false }
390 is Protocol.Event.SessionUpserted -> {
402 is Protocol.Event.SessionUpserted -> {
@@ -396,18 +408,22 @@ class OpencodeCore(
396 sessions.add(0, SessionView(event.info.id, title))
408 sessions.add(0, SessionView(event.info.id, title))
398 if (isCurrent(event.info.id)) currentSessionTitle = title
410 if (isCurrent(event.info.id)) currentSessionTitle = title
401− is Protocol.Event.SessionDeleted -> event.sessionId?.let { id ->
414+ is Protocol.Event.SessionDeleted -> {
415+ val id = event.sessionId ?: return false
402− sessions.removeAll { it.id == id }
416+ val removed = sessions.removeAll { it.id == id }
403 if (isCurrent(id)) {
417 if (isCurrent(id)) {
404 currentSessionId = null
418 currentSessionId = null
405 messages.clear()
419 messages.clear()
406 screen = Screen.Sessions
420 screen = Screen.Sessions
410− Protocol.Event.Other -> Unit
426+ Protocol.Event.Other -> return false