cc-haha/desktop/src/stores/terminalPanelStore.ts
程序员阿江(Relakkes) 35f9f0d0f8 feat: make project terminals open where users work
Desktop terminal access should behave like an IDE: active project sessions open a bottom panel in the session working directory while keeping a full terminal tab available for dedicated use. The panel has constrained resizing and cleanup so session tab state remains isolated, and terminal guidance points users to the bundled claude-haha command for extension setup.

Constraint: Desktop bundles the user-facing CLI as claude-haha while claude-sidecar remains internal
Rejected: Always opening a standalone terminal tab | loses the current project context and diverges from common IDE behavior
Rejected: Exposing claude-sidecar in terminal guidance | it is an internal launcher, not the supportable user command
Confidence: high
Scope-risk: moderate
Directive: Keep bottom terminals keyed by session id and pass session workDir/projectPath into spawned terminals
Tested: bun run check:desktop
Tested: git diff --check
Tested: Computer Use E2E against built macOS app during implementation
Not-tested: bun run quality:pr is blocked by existing branch policy requiring allow-cli-core-change approval
2026-05-05 19:05:36 +08:00

95 lines
2.5 KiB
TypeScript

import { create } from 'zustand'
export const TERMINAL_PANEL_DEFAULT_HEIGHT = 300
export const TERMINAL_PANEL_MIN_HEIGHT = 220
export const TERMINAL_PANEL_MAX_HEIGHT = 560
type TerminalPanelSessionState = {
isOpen: boolean
}
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
}
const DEFAULT_PANEL_STATE: TerminalPanelSessionState = {
isOpen: false,
}
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) => ({
panelBySession: {
...state.panelBySession,
[sessionId]: {
...getSessionPanelState(state.panelBySession, sessionId),
isOpen: true,
},
},
})),
closePanel: (sessionId) =>
set((state) => ({
panelBySession: {
...state.panelBySession,
[sessionId]: {
...getSessionPanelState(state.panelBySession, sessionId),
isOpen: false,
},
},
})),
togglePanel: (sessionId) =>
set((state) => {
const panel = getSessionPanelState(state.panelBySession, sessionId)
return {
panelBySession: {
...state.panelBySession,
[sessionId]: {
...panel,
isOpen: !panel.isOpen,
},
},
}
}),
setHeight: (height) => set({ height: clampTerminalPanelHeight(height) }),
clearSession: (sessionId) =>
set((state) => ({
panelBySession: removeRecordKey(state.panelBySession, sessionId),
})),
}))