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 <noreply@anthropic.com>
This commit is contained in:
Jinjing
2026-03-23 17:10:28 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 9853bba8ba
commit 82425df6ea
3 changed files with 39 additions and 19 deletions
+1 -1
View File
@@ -386,7 +386,7 @@ export default function Terminal(): React.JSX.Element | null {
>
{worktreeTabs.map((tab) => (
<TerminalPane
key={tab.id}
key={`${tab.id}-${tab.generation ?? 0}`}
tabId={tab.id}
worktreeId={worktree.id}
cwd={worktree.path}
@@ -235,6 +235,24 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
}
})
// 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)
+20 -18
View File
@@ -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<string, WorktreeMeta>
@@ -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
}