From 786482a00cffa0501977800af5472a89448f824c Mon Sep 17 00:00:00 2001 From: MocA-Love <64681295+MocA-Love@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:51:22 +0900 Subject: [PATCH 1/2] fix(desktop): restart v1 terminal stream subscription on reconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the cache-owned stream subscription dies (v1-terminal-cache.ts onError nulls the subscription and resets streamReady), only the initial attach path in useTerminalLifecycle re-runs startStream / setStreamReady / markTerminalSessionReady. handleRetryConnection was a standalone re-attach: it happily succeeded at the tRPC level, wrote a "[Reconnected]" marker to xterm, and left the cache-owned stream in a dead state. The resulting terminal looked healthy but never received another byte of stdout / exit / error. Long-running tail -f processes, dev servers, and shells waiting on idle commands silently stopped updating and the user had to restart the pane (or the whole app) to notice. The failure mode was especially pernicious for background builds — "[Reconnected]" gave the impression that recovery had worked. Restart the cache subscription and mark readiness at the end of the retry success path, mirroring the initial attach path. Gate the call on !result.isColdRestore: cold-restore returns from main without a real session, and the existing cold-restore block handles its own bookkeeping (and the replacement shell will run through handleStartShell, which already needs its own readiness plumbing — addressed separately). FORK NOTE: upstream regression. Revisit when upstream ships a coherent retry / cache-reset contract. --- .../Terminal/hooks/useTerminalColdRestore.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts index e18d0d9bdcd..eb7f16dda35 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts @@ -130,6 +130,27 @@ export function useTerminalColdRestore({ setConnectionError(null); currentXterm.writeln("\x1b[90m[Reconnected]\x1b[0m"); + // FORK NOTE: when the previous stream subscription died + // (v1-terminal-cache.ts onError nulls `subscription` and + // resets `streamReady`) only useTerminalLifecycle's initial + // attach path restarts it. handleRetryConnection is a + // standalone re-attach that used to succeed silently but + // left the cache without a live subscription — so the + // reconnected shell's stdout/exit/error events never + // reached the component and the user saw a terminal that + // displayed "[Reconnected]" but never produced any more + // output. Kick the cache-owned stream (and any + // session-readiness waiters) back into the ready state + // here — unconditionally, so the cold-restore branch + // below also gets live events. This mirrors the initial + // attach path in useTerminalLifecycle.ts which calls + // markSessionReady *before* inspecting result.isColdRestore. + // markSessionReady internally runs startStream + + // setStreamReady + markTerminalSessionReady, which are + // all idempotent (see v1-terminal-cache.ts), so running + // them in both reconnect branches is safe. + v1TerminalCache.markSessionReady(paneId); + if (result.isColdRestore) { const scrollback = result.snapshot?.snapshotAnsi ?? result.scrollback; From b013578a2938593d1bf532fa651b63560d836cf1 Mon Sep 17 00:00:00 2001 From: MocA-Love <64681295+MocA-Love@users.noreply.github.com> Date: Wed, 15 Apr 2026 20:40:58 +0900 Subject: [PATCH 2/2] fix(desktop): only mark session ready on real backend reconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review (PR#174): the previous unconditional markSessionReady() call in handleRetryConnection.onSuccess broke the PR #167 invariant that the cache must not flip to streamReady=true before a real backend session exists. Cold-restore reconnect responses come back with isColdRestore=true and no backend; setting the cache ready in that state lets a subsequent tab-switch remount take the isReattach fast-path and silently drop user keystrokes — exactly the bug PR #167 was meant to fix. Restore the !result.isColdRestore gate. The cold-restore reconnect path is still wired end-to-end because handleStartShell.onSuccess (in main, after PR #167) calls markSessionReady once the replacement shell spawns. --- .../Terminal/hooks/useTerminalColdRestore.ts | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts index eb7f16dda35..eed2fffe798 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts @@ -139,17 +139,20 @@ export function useTerminalColdRestore({ // reconnected shell's stdout/exit/error events never // reached the component and the user saw a terminal that // displayed "[Reconnected]" but never produced any more - // output. Kick the cache-owned stream (and any - // session-readiness waiters) back into the ready state - // here — unconditionally, so the cold-restore branch - // below also gets live events. This mirrors the initial - // attach path in useTerminalLifecycle.ts which calls - // markSessionReady *before* inspecting result.isColdRestore. - // markSessionReady internally runs startStream + - // setStreamReady + markTerminalSessionReady, which are - // all idempotent (see v1-terminal-cache.ts), so running - // them in both reconnect branches is safe. - v1TerminalCache.markSessionReady(paneId); + // output. + // + // Only mark the cache as session-ready when the daemon + // actually returned a real backend session. The cold + // restore branch below has no backend yet (PR #167 + // invariant: streamReady must not be set before a real + // shell exists, otherwise tab-switch remounts can take + // the isReattach fast-path and silently drop user + // keystrokes). The replacement shell created later by + // handleStartShell calls markSessionReady itself, so the + // cold-restore reconnect path is still covered end-to-end. + if (!result.isColdRestore) { + v1TerminalCache.markSessionReady(paneId); + } if (result.isColdRestore) { const scrollback =