From aa9f23782a7ffa4603126d3ba2e567c4b38d1d09 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Fri, 20 Feb 2026 20:48:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E5=8A=9F=E8=83=BD=E5=8F=8A=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=9B=BD=E9=99=85=E5=8C=96=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../terminal-dashboard/TerminalPane.tsx | 85 ++++++++++++++++++- ccw/frontend/src/locales/en/cli-manager.json | 1 + .../src/locales/en/terminal-dashboard.json | 6 +- ccw/frontend/src/locales/zh/cli-manager.json | 1 + .../src/locales/zh/terminal-dashboard.json | 6 +- .../src/stores/sessionManagerStore.ts | 38 ++++++++- ccw/frontend/src/types/terminal-dashboard.ts | 2 + ccw/src/core/services/cli-session-manager.ts | 6 +- 8 files changed, 136 insertions(+), 9 deletions(-) diff --git a/ccw/frontend/src/components/terminal-dashboard/TerminalPane.tsx b/ccw/frontend/src/components/terminal-dashboard/TerminalPane.tsx index 3aa29996..58804d93 100644 --- a/ccw/frontend/src/components/terminal-dashboard/TerminalPane.tsx +++ b/ccw/frontend/src/components/terminal-dashboard/TerminalPane.tsx @@ -22,10 +22,21 @@ import { Loader2, FileText, ArrowLeft, + LogOut, } from 'lucide-react'; import { cn } from '@/lib/utils'; import { TerminalInstance } from './TerminalInstance'; import { FilePreview } from '@/components/shared/FilePreview'; +import { + AlertDialog, + AlertDialogContent, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogAction, + AlertDialogCancel, +} from '@/components/ui/AlertDialog'; import { useTerminalGridStore, selectTerminalGridPanes, @@ -33,7 +44,6 @@ import { } from '@/stores/terminalGridStore'; import { useSessionManagerStore, - selectGroups, selectTerminalMetas, } from '@/stores/sessionManagerStore'; import { @@ -86,7 +96,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) { const isFileMode = displayMode === 'file' && filePath; // Session data - const groups = useSessionManagerStore(selectGroups); const terminalMetas = useSessionManagerStore(selectTerminalMetas); const sessions = useCliSessionStore((s) => s.sessions); @@ -94,10 +103,13 @@ export function TerminalPane({ paneId }: TerminalPaneProps) { const pauseSession = useSessionManagerStore((s) => s.pauseSession); const resumeSession = useSessionManagerStore((s) => s.resumeSession); const restartSession = useSessionManagerStore((s) => s.restartSession); + const closeSession = useSessionManagerStore((s) => s.closeSession); // Action loading states const [isRestarting, setIsRestarting] = useState(false); const [isTogglingPause, setIsTogglingPause] = useState(false); + const [isClosingSession, setIsClosingSession] = useState(false); + const [isCloseConfirmOpen, setIsCloseConfirmOpen] = useState(false); // File content for preview mode const { content: fileContent, isLoading: isFileLoading, error: fileError } = useFileContent(filePath, { @@ -118,14 +130,15 @@ export function TerminalPane({ paneId }: TerminalPaneProps) { const alertCount = meta?.alertCount ?? 0; // Build session options for dropdown + // Use sessions from cliSessionStore directly (all sessions, not just grouped ones) const sessionOptions = useMemo(() => { - const allSessionIds = groups.flatMap((g) => g.sessionIds); + const allSessionIds = Object.keys(sessions); return allSessionIds.map((sid) => { const s = sessions[sid]; const name = s ? (s.tool ? `${s.tool} - ${s.shellKind}` : s.shellKind) : sid; return { id: sid, name }; }); - }, [groups, sessions]); + }, [sessions]); // Handlers const handleFocus = useCallback(() => { @@ -141,7 +154,17 @@ export function TerminalPane({ paneId }: TerminalPaneProps) { }, [paneId, splitPane]); const handleClose = useCallback(() => { + // If pane has an active session, show confirmation dialog + if (sessionId) { + setIsCloseConfirmOpen(true); + } else { + closePane(paneId); + } + }, [paneId, sessionId, closePane]); + + const handleCloseConfirm = useCallback(() => { closePane(paneId); + setIsCloseConfirmOpen(false); }, [paneId, closePane]); const handleSessionChange = useCallback( @@ -189,6 +212,20 @@ export function TerminalPane({ paneId }: TerminalPaneProps) { } }, [sessionId, isTogglingPause, status, pauseSession, resumeSession]); + const handleCloseSession = useCallback(async () => { + if (!sessionId || isClosingSession) return; + setIsClosingSession(true); + try { + await closeSession(sessionId); + // Clear the pane's session after closing + assignSession(paneId, null); + } catch (error) { + console.error('[TerminalPane] Close session failed:', error); + } finally { + setIsClosingSession(false); + } + }, [sessionId, isClosingSession, closeSession, paneId, assignSession]); + // Handle back to terminal from file preview const handleBackToTerminal = useCallback(() => { setPaneDisplayMode(paneId, 'terminal'); @@ -329,6 +366,24 @@ export function TerminalPane({ paneId }: TerminalPaneProps) { > + {/* Close session button */} + )} {alertCount > 0 && !isFileMode && ( @@ -381,6 +436,28 @@ export function TerminalPane({ paneId }: TerminalPaneProps) { )} + + {/* Close Pane Confirmation Dialog */} + + + + + {formatMessage({ id: 'terminalDashboard.pane.closeConfirmTitle' })} + + + {formatMessage({ id: 'terminalDashboard.pane.closeConfirmMessage' })} + + + + + {formatMessage({ id: 'common.actions.cancel' })} + + + {formatMessage({ id: 'terminalDashboard.pane.closeConfirmAction' })} + + + + ); } diff --git a/ccw/frontend/src/locales/en/cli-manager.json b/ccw/frontend/src/locales/en/cli-manager.json index 7e92105d..4e279a14 100644 --- a/ccw/frontend/src/locales/en/cli-manager.json +++ b/ccw/frontend/src/locales/en/cli-manager.json @@ -227,6 +227,7 @@ "turns": "turns", "perTurnView": "Per-Turn View", "concatenatedView": "Concatenated View", + "nativeView": "Native View", "userPrompt": "User Prompt", "assistantResponse": "Assistant Response", "errors": "Errors", diff --git a/ccw/frontend/src/locales/en/terminal-dashboard.json b/ccw/frontend/src/locales/en/terminal-dashboard.json index ce829e5f..30cb005a 100644 --- a/ccw/frontend/src/locales/en/terminal-dashboard.json +++ b/ccw/frontend/src/locales/en/terminal-dashboard.json @@ -147,11 +147,15 @@ "splitVertical": "Split Down", "clearTerminal": "Clear Terminal", "closePane": "Close Pane", + "closeSession": "Close Session", "linkedIssue": "Linked Issue", "restart": "Restart Session", "pause": "Pause Session", "resume": "Resume Session", - "backToTerminal": "Back to terminal" + "backToTerminal": "Back to terminal", + "closeConfirmTitle": "Close Pane?", + "closeConfirmMessage": "The session will still exist and can be restored from the session tree.", + "closeConfirmAction": "Close Pane" }, "tabBar": { "noTabs": "No terminal sessions" diff --git a/ccw/frontend/src/locales/zh/cli-manager.json b/ccw/frontend/src/locales/zh/cli-manager.json index a2dac754..f25a6af9 100644 --- a/ccw/frontend/src/locales/zh/cli-manager.json +++ b/ccw/frontend/src/locales/zh/cli-manager.json @@ -227,6 +227,7 @@ "turns": "回合", "perTurnView": "分回合视图", "concatenatedView": "连接视图", + "nativeView": "原生视图", "userPrompt": "用户提示词", "assistantResponse": "助手响应", "errors": "错误", diff --git a/ccw/frontend/src/locales/zh/terminal-dashboard.json b/ccw/frontend/src/locales/zh/terminal-dashboard.json index e191d5e7..c2fe67a3 100644 --- a/ccw/frontend/src/locales/zh/terminal-dashboard.json +++ b/ccw/frontend/src/locales/zh/terminal-dashboard.json @@ -147,11 +147,15 @@ "splitVertical": "向下分割", "clearTerminal": "清屏", "closePane": "关闭窗格", + "closeSession": "关闭会话", "linkedIssue": "关联问题", "restart": "重启会话", "pause": "暂停会话", "resume": "恢复会话", - "backToTerminal": "返回终端" + "backToTerminal": "返回终端", + "closeConfirmTitle": "关闭窗格?", + "closeConfirmMessage": "会话仍将保留,可从会话树中恢复。", + "closeConfirmAction": "关闭窗格" }, "tabBar": { "noTabs": "暂无终端会话" diff --git a/ccw/frontend/src/stores/sessionManagerStore.ts b/ccw/frontend/src/stores/sessionManagerStore.ts index 4b37b9a6..505f3622 100644 --- a/ccw/frontend/src/stores/sessionManagerStore.ts +++ b/ccw/frontend/src/stores/sessionManagerStore.ts @@ -285,6 +285,13 @@ export const useSessionManagerStore = create()( shellKind: session.shellKind, }; + // Map shellKind to preferredShell for API + const mapShellKind = (kind: string): 'bash' | 'pwsh' | 'cmd' => { + if (kind === 'pwsh' || kind === 'powershell') return 'pwsh'; + if (kind === 'cmd') return 'cmd'; + return 'bash'; // 'git-bash', 'wsl-bash', or fallback + }; + try { // Close existing session await closeCliSession(terminalId, projectPath ?? undefined); @@ -293,7 +300,7 @@ export const useSessionManagerStore = create()( const result = await createCliSession( { workingDir: sessionConfig.workingDir, - preferredShell: sessionConfig.shellKind === 'powershell' ? 'pwsh' : 'bash', + preferredShell: mapShellKind(sessionConfig.shellKind), tool: sessionConfig.tool, model: sessionConfig.model, resumeKey: sessionConfig.resumeKey, @@ -325,6 +332,35 @@ export const useSessionManagerStore = create()( throw error; } }, + + closeSession: async (terminalId: string) => { + const projectPath = selectProjectPath(useWorkflowStore.getState()); + const cliStore = useCliSessionStore.getState(); + + try { + // Call backend API to terminate PTY session + await closeCliSession(terminalId, projectPath ?? undefined); + + // Remove session from cliSessionStore + cliStore.removeSession(terminalId); + + // Remove terminal meta + set( + (state) => { + const nextMetas = { ...state.terminalMetas }; + delete nextMetas[terminalId]; + return { terminalMetas: nextMetas }; + }, + false, + 'closeSession' + ); + } catch (error) { + if (import.meta.env.DEV) { + console.error('[SessionManager] closeSession error:', error); + } + throw error; + } + }, }), { name: 'SessionManagerStore' } ) diff --git a/ccw/frontend/src/types/terminal-dashboard.ts b/ccw/frontend/src/types/terminal-dashboard.ts index 92151eae..b2185e7e 100644 --- a/ccw/frontend/src/types/terminal-dashboard.ts +++ b/ccw/frontend/src/types/terminal-dashboard.ts @@ -89,6 +89,8 @@ export interface SessionManagerActions { resumeSession: (terminalId: string) => Promise; /** Restart a terminal session (close and recreate with same config) */ restartSession: (terminalId: string) => Promise; + /** Close and terminate a terminal session permanently */ + closeSession: (terminalId: string) => Promise; } export type SessionManagerStore = SessionManagerState & SessionManagerActions; diff --git a/ccw/src/core/services/cli-session-manager.ts b/ccw/src/core/services/cli-session-manager.ts index 038497a4..bdb7b16b 100644 --- a/ccw/src/core/services/cli-session-manager.ts +++ b/ccw/src/core/services/cli-session-manager.ts @@ -280,8 +280,10 @@ export class CliSessionManager { } else { // Legacy shell session: spawn bash/pwsh - const preferredShell = options.preferredShell ?? 'bash'; - const picked = pickShell(preferredShell); + // Note: 'cmd' is for CLI tools only, for legacy shells we default to bash + const shellPreference = options.preferredShell ?? 'bash'; + const preferredShell = shellPreference === 'cmd' ? 'bash' : shellPreference; + const picked = pickShell(preferredShell as 'bash' | 'pwsh'); shellKind = picked.shellKind; file = picked.file; args = picked.args;