From 82425df6eafd0bff5e4e6dc8fcaaedee783beaa0 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:10:28 -0700 Subject: [PATCH] fix: remount terminal panes after shutdown to restore PTY connections (#73) When all PTYs are dead after shutdown, bump a generation counter on terminal tabs so React remounts TerminalPane components with fresh PTY connections instead of showing stale terminals. Also converts interfaces to type aliases in types.ts to satisfy linter. Co-authored-by: Claude Opus 4.6 --- src/renderer/src/components/Terminal.tsx | 2 +- src/renderer/src/store/slices/worktrees.ts | 18 ++++++++++ src/shared/types.ts | 38 ++++++++++++---------- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx index c0f1760f38c..b68712c42b3 100644 --- a/src/renderer/src/components/Terminal.tsx +++ b/src/renderer/src/components/Terminal.tsx @@ -386,7 +386,7 @@ export default function Terminal(): React.JSX.Element | null { > {worktreeTabs.map((tab) => ( } }) + // If the worktree has tabs but all PTYs are dead (e.g. after shutdown), + // bump generation so TerminalPanes remount with fresh PTY connections. + if (worktreeId) { + const tabs = get().tabsByWorktree[worktreeId] ?? [] + const allDead = tabs.length > 0 && tabs.every((tab) => !tab.ptyId) + if (allDead) { + set((s) => ({ + tabsByWorktree: { + ...s.tabsByWorktree, + [worktreeId]: (s.tabsByWorktree[worktreeId] ?? []).map((tab) => ({ + ...tab, + generation: (tab.generation ?? 0) + 1 + })) + } + })) + } + } + // Refresh GitHub data (PR + issue status) when switching to a different worktree if (worktreeId && worktreeId !== prevActiveId) { get().refreshGitHubForWorktree(worktreeId) diff --git a/src/shared/types.ts b/src/shared/types.ts index 718a6ec9b11..aae2959011b 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -1,5 +1,5 @@ // ─── Repo ──────────────────────────────────────────────────────────── -export interface Repo { +export type Repo = { id: string path: string displayName: string @@ -11,7 +11,7 @@ export interface Repo { } // ─── Worktree (git-level) ──────────────────────────────────────────── -export interface GitWorktreeInfo { +export type GitWorktreeInfo = { path: string head: string branch: string @@ -19,7 +19,7 @@ export interface GitWorktreeInfo { } // ─── Worktree (app-level, enriched) ────────────────────────────────── -export interface Worktree extends GitWorktreeInfo { +export type Worktree = { id: string // `${repoId}::${path}` repoId: string displayName: string @@ -29,10 +29,10 @@ export interface Worktree extends GitWorktreeInfo { isArchived: boolean isUnread: boolean sortOrder: number -} +} & GitWorktreeInfo // ─── Worktree metadata (persisted user-authored fields only) ───────── -export interface WorktreeMeta { +export type WorktreeMeta = { displayName: string comment: string linkedIssue: number | null @@ -43,7 +43,7 @@ export interface WorktreeMeta { } // ─── Terminal Tab ──────────────────────────────────────────────────── -export interface TerminalTab { +export type TerminalTab = { id: string ptyId: string | null worktreeId: string @@ -52,6 +52,8 @@ export interface TerminalTab { color: string | null sortOrder: number createdAt: number + /** Bumped on shutdown so TerminalPane remounts with a fresh PTY. */ + generation?: number } export type TerminalPaneSplitDirection = 'vertical' | 'horizontal' @@ -70,13 +72,13 @@ export type TerminalPaneLayoutNode = ratio?: number } -export interface TerminalLayoutSnapshot { +export type TerminalLayoutSnapshot = { root: TerminalPaneLayoutNode | null activeLeafId: string | null expandedLeafId: string | null } -export interface WorkspaceSessionState { +export type WorkspaceSessionState = { activeRepoId: string | null activeWorktreeId: string | null activeTabId: string | null @@ -89,7 +91,7 @@ export type PRState = 'open' | 'closed' | 'merged' | 'draft' export type IssueState = 'open' | 'closed' export type CheckStatus = 'pending' | 'success' | 'failure' | 'neutral' -export interface PRInfo { +export type PRInfo = { number: number title: string state: PRState @@ -98,7 +100,7 @@ export interface PRInfo { updatedAt: string } -export interface IssueInfo { +export type IssueInfo = { number: number title: string state: IssueState @@ -107,14 +109,14 @@ export interface IssueInfo { } // ─── Hooks (orca.yaml) ────────────────────────────────────────────── -export interface OrcaHooks { +export type OrcaHooks = { scripts: { setup?: string // Runs after worktree is created archive?: string // Runs before worktree is archived } } -export interface RepoHookSettings { +export type RepoHookSettings = { mode: 'auto' | 'override' scripts: { setup: string @@ -133,7 +135,7 @@ export type UpdateStatus = | { state: 'error'; message: string; userInitiated?: boolean } // ─── Settings ──────────────────────────────────────────────────────── -export interface GlobalSettings { +export type GlobalSettings = { workspaceDir: string nestWorkspaces: boolean branchPrefix: 'git-username' | 'custom' | 'none' @@ -155,7 +157,7 @@ export interface GlobalSettings { terminalScrollbackBytes: number } -export interface PersistedUIState { +export type PersistedUIState = { lastActiveRepoId: string | null lastActiveWorktreeId: string | null sidebarWidth: number @@ -166,7 +168,7 @@ export interface PersistedUIState { } // ─── Persistence shape ────────────────────────────────────────────── -export interface PersistedState { +export type PersistedState = { schemaVersion: number repos: Repo[] worktreeMeta: Record @@ -180,7 +182,7 @@ export interface PersistedState { } // ─── Filesystem ───────────────────────────────────────────── -export interface DirEntry { +export type DirEntry = { name: string isDirectory: boolean isSymlink: boolean @@ -190,14 +192,14 @@ export interface DirEntry { export type GitFileStatus = 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked' | 'copied' export type GitStagingArea = 'staged' | 'unstaged' | 'untracked' -export interface GitStatusEntry { +export type GitStatusEntry = { path: string status: GitFileStatus area: GitStagingArea oldPath?: string } -export interface GitDiffResult { +export type GitDiffResult = { originalContent: string modifiedContent: string }