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;