Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Terminal as XTerm } from "@xterm/xterm";
import { useCallback, useRef, useState } from "react";
import { rejectTerminalSessionReady } from "renderer/lib/terminal/session-readiness";
import { electronTrpcClient as trpcClient } from "renderer/lib/trpc-client";
import { isTerminalAttachCanceledMessage } from "../attach-cancel";
import { coldRestoreState } from "../state";
Expand All @@ -9,6 +10,7 @@ import type {
TerminalStreamEvent,
} from "../types";
import { scrollToBottom } from "../utils";
import * as v1TerminalCache from "../v1-terminal-cache";

export interface UseTerminalColdRestoreOptions {
paneId: string;
Expand Down Expand Up @@ -208,6 +210,14 @@ export function useTerminalColdRestore({
pendingInitialStateRef.current = result;
maybeApplyInitialState();

// FORK NOTE: now that handleStartShell has a real backend
// session, mark the v1 cache + session-readiness waiters as
// ready. useTerminalLifecycle.ts intentionally defers this
// for the cold-restore path so that a tab-switch remount
// does not take the isReattach fast-path before a real
// shell exists.
v1TerminalCache.markSessionReady(paneId);

setIsRestoredMode(false);
coldRestoreState.delete(paneId);

Expand All @@ -226,6 +236,10 @@ export function useTerminalColdRestore({
setConnectionError(error.message || "Failed to start shell");
setIsRestoredMode(false);
coldRestoreState.delete(paneId);
rejectTerminalSessionReady(
paneId,
new Error(error.message || "Failed to start shell"),
);
isStreamReadyRef.current = true;
flushPendingEvents();
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { writeCommandInPane } from "renderer/lib/terminal/launch-command";
import type { DetectedLink } from "renderer/lib/terminal/links";
import {
clearTerminalSessionReady,
markTerminalSessionReady,
rejectTerminalSessionReady,
} from "renderer/lib/terminal/session-readiness";
import { electronTrpcClient } from "renderer/lib/trpc-client";
Expand Down Expand Up @@ -256,9 +255,20 @@ export function useTerminalLifecycle({
// Only treat as reattach when the stream is still alive (subscription ≠ null).
// If the stream died while the tab was hidden (onError sets subscription=null),
// we must go through the full create/attach path to restart it.
//
// FORK NOTE: Also block the fast-path while the pane is in cold-restore
// mode. Cold restore returns `isColdRestore: true` without creating a
// backend session, but the onSuccess handler below still marks the
// cache as `streamReady=true`. On the next mount (e.g. after a tab
// switch unmount) the fast-path would otherwise skip createOrAttach
// entirely, leaving a stream with no backend → user typing is silently
// dropped by `electronTrpcClient.terminal.write`. Forcing the full
// attach path here lets `handleStartShell` run and spawn a real shell.
const hasPendingColdRestore = coldRestoreState.has(paneId);
const isReattach =
cachedBeforeCreate?.streamReady === true &&
cachedBeforeCreate.subscription !== null;
cachedBeforeCreate.subscription !== null &&
!hasPendingColdRestore;
if (DEBUG_TERMINAL) {
console.log(`[Terminal] isReattach=${isReattach} paneId=${paneId}`);
}
Expand Down Expand Up @@ -602,13 +612,14 @@ export function useTerminalLifecycle({
setConnectionError(null);
clearPaneInitialDataRef.current(paneId);

// Start the cache-owned stream subscription now that the
// backend session exists, and mark it ready so events
// flow through the component's registered handler.
v1TerminalCache.startStream(paneId);
v1TerminalCache.setStreamReady(paneId);
markTerminalSessionReady(paneId);

// FORK NOTE: Do NOT mark the cache as streamReady here
// yet. Cold-restore responses come back without a real
// backend session, so we must defer startStream /
// setStreamReady / markTerminalSessionReady until
// handleStartShell spawns an actual shell. Marking
// readiness up front caused tab-switch remounts to enter
// the isReattach fast-path with no backend, silently
// dropping every user keystroke.
const storedColdRestore = coldRestoreState.get(paneId);
if (storedColdRestore?.isRestored) {
setIsRestoredMode(true);
Expand Down Expand Up @@ -640,6 +651,10 @@ export function useTerminalLifecycle({
return;
}

// Real backend session is live — safe to start the stream
// subscription and unblock waiters.
v1TerminalCache.markSessionReady(paneId);

pendingInitialStateRef.current = result;
maybeApplyInitialState();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Unsubscribable } from "@trpc/server/observable";
import type { FitAddon } from "@xterm/addon-fit";
import type { SearchAddon } from "@xterm/addon-search";
import type { Terminal as XTerm } from "@xterm/xterm";
import { markTerminalSessionReady } from "renderer/lib/terminal/session-readiness";
import { electronTrpcClient } from "renderer/lib/trpc-client";
import { DEBUG_TERMINAL } from "./config";
import { type CreateTerminalOptions, createTerminalInWrapper } from "./helpers";
Expand Down Expand Up @@ -266,6 +267,21 @@ export function setStreamReady(paneId: string): void {
}
}

/**
* Mark a pane as session-ready: start the tRPC stream, flip the cache's
* `streamReady` flag, and resolve any {@link waitForTerminalSessionReady}
* waiters in one step.
*
* FORK NOTE: centralizes the three-call sequence so the cold-restore and
* normal attach paths can't drift — see useTerminalLifecycle.ts and
* useTerminalColdRestore.ts.
*/
export function markSessionReady(paneId: string): void {
startStream(paneId);
setStreamReady(paneId);
markTerminalSessionReady(paneId);
}

/**
* Register event handlers from the mounted Terminal component.
* Returns any lifecycle events (exit, error, disconnect) that were
Expand Down
Loading