From 3a160dad7186b33f470ed98225e789edcec3c4ec Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 19:51:55 +0000 Subject: [PATCH] fix(android): keep a task's terminal streaming while another screen shows it Two screens can show one agent's terminal at once: reopening a task while its previous screen still animates out, or the task pager replacing the lone-task view when the task list changes. The new screen reused the buffer, then the old one's dispose released it, removing the buffer and unsubscribing. The open screen was left drawing a buffer nothing fed, so the terminal showed only its empty background. Count terminal watchers, as chat watchers already are, and stop streaming only when the last screen lets go. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012boc2XLLaQiSeNwAqk8YYs --- .../com/parallelcode/phone/RemoteClient.kt | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/com/parallelcode/phone/RemoteClient.kt b/android/app/src/main/java/com/parallelcode/phone/RemoteClient.kt index b2626840..c7e7e3e3 100644 --- a/android/app/src/main/java/com/parallelcode/phone/RemoteClient.kt +++ b/android/app/src/main/java/com/parallelcode/phone/RemoteClient.kt @@ -140,6 +140,8 @@ class RemoteClient( private var handshakeJob: Job? = null private val terminalBuffers = mutableMapOf() private val subscribedAgents = mutableSetOf() + /** Screens showing each agent's terminal; it streams until the last one lets go. */ + private val terminalWatchers = mutableMapOf() private val chats = mutableMapOf>() private val chatWatchers = mutableMapOf() /** The agent whose PTY takes this phone's terminal size, and that size (see [setViewSize]). */ @@ -539,16 +541,29 @@ class RemoteClient( /** * Stream an agent's terminal while it is on screen. Only viewed terminals stream: opening one * starts from the desktop's rendered snapshot, so nothing needs buffering in the background. - * Pair with [releaseTerminal]. + * Pair each call with [releaseTerminal]: two screens can show one terminal at once (a task + * reopened while its last screen still animates out, the pager taking over from a lone task), + * and the screen leaving last must not drop the buffer the other one is drawing. */ fun getTerminalBuffer(agentId: String): TerminalBuffer { + terminalWatchers[agentId] = (terminalWatchers[agentId] ?: 0) + 1 + return streamTerminal(agentId) + } + + private fun streamTerminal(agentId: String): TerminalBuffer { val buffer = terminalBuffers.getOrPut(agentId) { TerminalBuffer(agentId) } subscribeAgent(agentId) return buffer } - /** Stop streaming a terminal that left the screen. */ + /** Stop streaming a terminal once no screen shows it. */ fun releaseTerminal(agentId: String) { + val watchers = (terminalWatchers[agentId] ?: 0) - 1 + if (watchers > 0) { + terminalWatchers[agentId] = watchers + return + } + terminalWatchers.remove(agentId) terminalBuffers.remove(agentId) if (subscribedAgents.remove(agentId) && socketOpen) { send(JSONObject().put("type", "unsubscribe").put("agentId", agentId)) @@ -627,7 +642,7 @@ class RemoteClient( /** Stream an agent's terminal. Retained for backwards compatibility. */ fun watchTerminal(agentId: String, listener: TerminalListener): () -> Unit { - getTerminalBuffer(agentId) + streamTerminal(agentId) val listeners = terminalListeners.getOrPut(agentId) { mutableSetOf() } listeners.add(listener) return { @@ -810,7 +825,7 @@ class RemoteClient( list.map { if (it.agentId == msg.agentId) it.copy(running = msg.running, exitCode = msg.exitCode) else it } } if (msg.running) { - getTerminalBuffer(msg.agentId) + streamTerminal(msg.agentId) } } is ServerMessage.Scrollback -> {