cc-haha/desktop/src/components/layout/ContentRouter.tsx
程序员阿江(Relakkes) 76a4ca5d30 fix: preserve desktop terminal sessions across panel moves
Users can be typing in the docked terminal when they hide the panel or promote it into a tab. The old component-owned lifecycle treated those UI moves as terminal teardown and spawned a fresh shell afterward.

This moves terminal ownership into a small runtime registry keyed by panel or tab identity, keeps docked terminals mounted while hidden, and transfers the runtime id to the terminal tab when promoted. Closing the owning tab or session still releases the PTY.

Constraint: Keep native PTY behavior unchanged and fix this in the desktop React lifecycle layer.
Rejected: Persist terminal tabs through localStorage | runtime PTYs are process-local and should not be restored after app restart.
Confidence: high
Scope-risk: moderate
Directive: Do not tie terminal process lifetime to panel visibility; only explicit owning-surface close/restart should destroy it.
Tested: cd desktop && bun run test -- src/pages/TerminalSettings.test.tsx src/pages/ActiveSession.test.tsx src/components/layout/ContentRouter.test.tsx src/stores/tabStore.test.ts
Tested: bun run check:desktop
Not-tested: Manual Tauri window PTY smoke.
2026-05-25 22:33:22 +08:00

59 lines
2.1 KiB
TypeScript

import type { ReactNode } from 'react'
import { useTabStore } from '../../stores/tabStore'
import { EmptySession } from '../../pages/EmptySession'
import { ActiveSession } from '../../pages/ActiveSession'
import { ScheduledTasks } from '../../pages/ScheduledTasks'
import { Settings } from '../../pages/Settings'
import { TerminalSettings } from '../../pages/TerminalSettings'
export function ContentRouter() {
const activeTabId = useTabStore((s) => s.activeTabId)
const tabs = useTabStore((s) => s.tabs)
const activeTabType = tabs.find((t) => t.sessionId === activeTabId)?.type
const terminalTabs = tabs.filter((tab) => tab.type === 'terminal')
let page: ReactNode = null
if (!activeTabId || !activeTabType) {
page = <EmptySession />
} else if (activeTabType === 'settings') {
page = <Settings />
} else if (activeTabType === 'scheduled') {
page = <ScheduledTasks />
} else if (activeTabType !== 'terminal') {
page = <ActiveSession />
}
return (
<div className="relative min-h-0 flex-1 overflow-hidden">
{page && (
<div className="absolute inset-0 z-10 flex min-h-0 flex-col overflow-hidden">
{page}
</div>
)}
{terminalTabs.map((tab) => {
const active = tab.sessionId === activeTabId
const visible = activeTabType === 'terminal' && active
return (
<div
key={tab.sessionId}
aria-hidden={!visible}
data-testid={`terminal-tab-panel-${tab.sessionId}`}
className={`absolute inset-0 flex min-h-0 flex-col overflow-hidden ${
visible ? 'z-20 opacity-100' : 'pointer-events-none z-0 opacity-0'
}`}
>
<TerminalSettings
active={active}
cwd={tab.terminalCwd}
runtimeId={tab.terminalRuntimeId ?? tab.sessionId}
workspace
testId={`terminal-host-${tab.sessionId}`}
onNewTerminal={() => useTabStore.getState().openTerminalTab(tab.terminalCwd)}
/>
</div>
)
})}
</div>
)
}