mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
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
95 lines
2.5 KiB
TypeScript
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),
|
|
})),
|
|
}))
|