mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
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.
136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
import { create } from 'zustand'
|
|
import { destroyTerminalRuntime } from '../lib/terminalRuntime'
|
|
|
|
export const TERMINAL_PANEL_DEFAULT_HEIGHT = 300
|
|
export const TERMINAL_PANEL_MIN_HEIGHT = 220
|
|
export const TERMINAL_PANEL_MAX_HEIGHT = 560
|
|
export const SESSION_TERMINAL_RUNTIME_PREFIX = '__session_terminal__'
|
|
|
|
type TerminalPanelSessionState = {
|
|
isOpen: boolean
|
|
runtimeId?: string
|
|
}
|
|
|
|
type TerminalPanelStore = {
|
|
panelBySession: Record<string, TerminalPanelSessionState | undefined>
|
|
height: number
|
|
|
|
isPanelOpen: (sessionId: string) => boolean
|
|
openPanel: (sessionId: string) => void
|
|
closePanel: (sessionId: string) => void
|
|
togglePanel: (sessionId: string) => void
|
|
setHeight: (height: number) => void
|
|
clearSession: (sessionId: string) => void
|
|
detachRuntime: (sessionId: string) => void
|
|
getPanelRuntimeId: (sessionId: string) => string | undefined
|
|
}
|
|
|
|
const DEFAULT_PANEL_STATE: TerminalPanelSessionState = {
|
|
isOpen: false,
|
|
}
|
|
|
|
function createSessionTerminalRuntimeId(sessionId: string) {
|
|
return `${SESSION_TERMINAL_RUNTIME_PREFIX}${sessionId}`
|
|
}
|
|
|
|
function getSessionPanelState(
|
|
panelBySession: Record<string, TerminalPanelSessionState | undefined>,
|
|
sessionId: string,
|
|
) {
|
|
return panelBySession[sessionId] ?? DEFAULT_PANEL_STATE
|
|
}
|
|
|
|
function removeRecordKey<T>(record: Record<string, T>, key: string) {
|
|
if (!(key in record)) return record
|
|
const { [key]: _removed, ...rest } = record
|
|
return rest
|
|
}
|
|
|
|
export function clampTerminalPanelHeight(height: number) {
|
|
if (!Number.isFinite(height)) return TERMINAL_PANEL_DEFAULT_HEIGHT
|
|
const rounded = Math.round(height)
|
|
return Math.min(TERMINAL_PANEL_MAX_HEIGHT, Math.max(TERMINAL_PANEL_MIN_HEIGHT, rounded))
|
|
}
|
|
|
|
export const useTerminalPanelStore = create<TerminalPanelStore>((set, get) => ({
|
|
panelBySession: {},
|
|
height: TERMINAL_PANEL_DEFAULT_HEIGHT,
|
|
|
|
isPanelOpen: (sessionId) => getSessionPanelState(get().panelBySession, sessionId).isOpen,
|
|
|
|
openPanel: (sessionId) =>
|
|
set((state) => {
|
|
const panel = getSessionPanelState(state.panelBySession, sessionId)
|
|
return {
|
|
panelBySession: {
|
|
...state.panelBySession,
|
|
[sessionId]: {
|
|
...panel,
|
|
isOpen: true,
|
|
runtimeId: panel.runtimeId ?? createSessionTerminalRuntimeId(sessionId),
|
|
},
|
|
},
|
|
}
|
|
}),
|
|
|
|
closePanel: (sessionId) =>
|
|
set((state) => {
|
|
const panel = getSessionPanelState(state.panelBySession, sessionId)
|
|
return {
|
|
panelBySession: {
|
|
...state.panelBySession,
|
|
[sessionId]: {
|
|
...panel,
|
|
isOpen: false,
|
|
},
|
|
},
|
|
}
|
|
}),
|
|
|
|
togglePanel: (sessionId) =>
|
|
set((state) => {
|
|
const panel = getSessionPanelState(state.panelBySession, sessionId)
|
|
return {
|
|
panelBySession: {
|
|
...state.panelBySession,
|
|
[sessionId]: {
|
|
...panel,
|
|
isOpen: !panel.isOpen,
|
|
runtimeId: panel.runtimeId ?? createSessionTerminalRuntimeId(sessionId),
|
|
},
|
|
},
|
|
}
|
|
}),
|
|
|
|
setHeight: (height) => set({ height: clampTerminalPanelHeight(height) }),
|
|
|
|
clearSession: (sessionId) =>
|
|
set((state) => {
|
|
const runtimeId = state.panelBySession[sessionId]?.runtimeId
|
|
if (runtimeId) {
|
|
destroyTerminalRuntime(runtimeId)
|
|
}
|
|
return {
|
|
panelBySession: removeRecordKey(state.panelBySession, sessionId),
|
|
}
|
|
}),
|
|
|
|
detachRuntime: (sessionId) =>
|
|
set((state) => {
|
|
const panel = state.panelBySession[sessionId]
|
|
if (!panel) return state
|
|
return {
|
|
panelBySession: {
|
|
...state.panelBySession,
|
|
[sessionId]: {
|
|
...panel,
|
|
isOpen: false,
|
|
runtimeId: undefined,
|
|
},
|
|
},
|
|
}
|
|
}),
|
|
|
|
getPanelRuntimeId: (sessionId) => get().panelBySession[sessionId]?.runtimeId,
|
|
}))
|