feat: 添加会话关闭功能及确认对话框,更新相关国际化文本

This commit is contained in:
catlog22
2026-02-20 20:48:24 +08:00
parent d6bf941113
commit aa9f23782a
8 changed files with 136 additions and 9 deletions

View File

@@ -22,10 +22,21 @@ import {
Loader2, Loader2,
FileText, FileText,
ArrowLeft, ArrowLeft,
LogOut,
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { TerminalInstance } from './TerminalInstance'; import { TerminalInstance } from './TerminalInstance';
import { FilePreview } from '@/components/shared/FilePreview'; import { FilePreview } from '@/components/shared/FilePreview';
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogAction,
AlertDialogCancel,
} from '@/components/ui/AlertDialog';
import { import {
useTerminalGridStore, useTerminalGridStore,
selectTerminalGridPanes, selectTerminalGridPanes,
@@ -33,7 +44,6 @@ import {
} from '@/stores/terminalGridStore'; } from '@/stores/terminalGridStore';
import { import {
useSessionManagerStore, useSessionManagerStore,
selectGroups,
selectTerminalMetas, selectTerminalMetas,
} from '@/stores/sessionManagerStore'; } from '@/stores/sessionManagerStore';
import { import {
@@ -86,7 +96,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
const isFileMode = displayMode === 'file' && filePath; const isFileMode = displayMode === 'file' && filePath;
// Session data // Session data
const groups = useSessionManagerStore(selectGroups);
const terminalMetas = useSessionManagerStore(selectTerminalMetas); const terminalMetas = useSessionManagerStore(selectTerminalMetas);
const sessions = useCliSessionStore((s) => s.sessions); const sessions = useCliSessionStore((s) => s.sessions);
@@ -94,10 +103,13 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
const pauseSession = useSessionManagerStore((s) => s.pauseSession); const pauseSession = useSessionManagerStore((s) => s.pauseSession);
const resumeSession = useSessionManagerStore((s) => s.resumeSession); const resumeSession = useSessionManagerStore((s) => s.resumeSession);
const restartSession = useSessionManagerStore((s) => s.restartSession); const restartSession = useSessionManagerStore((s) => s.restartSession);
const closeSession = useSessionManagerStore((s) => s.closeSession);
// Action loading states // Action loading states
const [isRestarting, setIsRestarting] = useState(false); const [isRestarting, setIsRestarting] = useState(false);
const [isTogglingPause, setIsTogglingPause] = useState(false); const [isTogglingPause, setIsTogglingPause] = useState(false);
const [isClosingSession, setIsClosingSession] = useState(false);
const [isCloseConfirmOpen, setIsCloseConfirmOpen] = useState(false);
// File content for preview mode // File content for preview mode
const { content: fileContent, isLoading: isFileLoading, error: fileError } = useFileContent(filePath, { const { content: fileContent, isLoading: isFileLoading, error: fileError } = useFileContent(filePath, {
@@ -118,14 +130,15 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
const alertCount = meta?.alertCount ?? 0; const alertCount = meta?.alertCount ?? 0;
// Build session options for dropdown // Build session options for dropdown
// Use sessions from cliSessionStore directly (all sessions, not just grouped ones)
const sessionOptions = useMemo(() => { const sessionOptions = useMemo(() => {
const allSessionIds = groups.flatMap((g) => g.sessionIds); const allSessionIds = Object.keys(sessions);
return allSessionIds.map((sid) => { return allSessionIds.map((sid) => {
const s = sessions[sid]; const s = sessions[sid];
const name = s ? (s.tool ? `${s.tool} - ${s.shellKind}` : s.shellKind) : sid; const name = s ? (s.tool ? `${s.tool} - ${s.shellKind}` : s.shellKind) : sid;
return { id: sid, name }; return { id: sid, name };
}); });
}, [groups, sessions]); }, [sessions]);
// Handlers // Handlers
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {
@@ -141,7 +154,17 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
}, [paneId, splitPane]); }, [paneId, splitPane]);
const handleClose = useCallback(() => { 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); closePane(paneId);
setIsCloseConfirmOpen(false);
}, [paneId, closePane]); }, [paneId, closePane]);
const handleSessionChange = useCallback( const handleSessionChange = useCallback(
@@ -189,6 +212,20 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
} }
}, [sessionId, isTogglingPause, status, pauseSession, resumeSession]); }, [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 // Handle back to terminal from file preview
const handleBackToTerminal = useCallback(() => { const handleBackToTerminal = useCallback(() => {
setPaneDisplayMode(paneId, 'terminal'); setPaneDisplayMode(paneId, 'terminal');
@@ -329,6 +366,24 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
> >
<Eraser className="w-3.5 h-3.5" /> <Eraser className="w-3.5 h-3.5" />
</button> </button>
{/* Close session button */}
<button
onClick={handleCloseSession}
disabled={isClosingSession}
className={cn(
'p-1 rounded hover:bg-muted transition-colors',
isClosingSession
? 'text-muted-foreground/50'
: 'text-muted-foreground hover:text-destructive'
)}
title={formatMessage({ id: 'terminalDashboard.pane.closeSession' })}
>
{isClosingSession ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<LogOut className="w-3.5 h-3.5" />
)}
</button>
</> </>
)} )}
{alertCount > 0 && !isFileMode && ( {alertCount > 0 && !isFileMode && (
@@ -381,6 +436,28 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
</div> </div>
</div> </div>
)} )}
{/* Close Pane Confirmation Dialog */}
<AlertDialog open={isCloseConfirmOpen} onOpenChange={setIsCloseConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{formatMessage({ id: 'terminalDashboard.pane.closeConfirmTitle' })}
</AlertDialogTitle>
<AlertDialogDescription>
{formatMessage({ id: 'terminalDashboard.pane.closeConfirmMessage' })}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
{formatMessage({ id: 'common.actions.cancel' })}
</AlertDialogCancel>
<AlertDialogAction onClick={handleCloseConfirm}>
{formatMessage({ id: 'terminalDashboard.pane.closeConfirmAction' })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
); );
} }

View File

@@ -227,6 +227,7 @@
"turns": "turns", "turns": "turns",
"perTurnView": "Per-Turn View", "perTurnView": "Per-Turn View",
"concatenatedView": "Concatenated View", "concatenatedView": "Concatenated View",
"nativeView": "Native View",
"userPrompt": "User Prompt", "userPrompt": "User Prompt",
"assistantResponse": "Assistant Response", "assistantResponse": "Assistant Response",
"errors": "Errors", "errors": "Errors",

View File

@@ -147,11 +147,15 @@
"splitVertical": "Split Down", "splitVertical": "Split Down",
"clearTerminal": "Clear Terminal", "clearTerminal": "Clear Terminal",
"closePane": "Close Pane", "closePane": "Close Pane",
"closeSession": "Close Session",
"linkedIssue": "Linked Issue", "linkedIssue": "Linked Issue",
"restart": "Restart Session", "restart": "Restart Session",
"pause": "Pause Session", "pause": "Pause Session",
"resume": "Resume 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": { "tabBar": {
"noTabs": "No terminal sessions" "noTabs": "No terminal sessions"

View File

@@ -227,6 +227,7 @@
"turns": "回合", "turns": "回合",
"perTurnView": "分回合视图", "perTurnView": "分回合视图",
"concatenatedView": "连接视图", "concatenatedView": "连接视图",
"nativeView": "原生视图",
"userPrompt": "用户提示词", "userPrompt": "用户提示词",
"assistantResponse": "助手响应", "assistantResponse": "助手响应",
"errors": "错误", "errors": "错误",

View File

@@ -147,11 +147,15 @@
"splitVertical": "向下分割", "splitVertical": "向下分割",
"clearTerminal": "清屏", "clearTerminal": "清屏",
"closePane": "关闭窗格", "closePane": "关闭窗格",
"closeSession": "关闭会话",
"linkedIssue": "关联问题", "linkedIssue": "关联问题",
"restart": "重启会话", "restart": "重启会话",
"pause": "暂停会话", "pause": "暂停会话",
"resume": "恢复会话", "resume": "恢复会话",
"backToTerminal": "返回终端" "backToTerminal": "返回终端",
"closeConfirmTitle": "关闭窗格?",
"closeConfirmMessage": "会话仍将保留,可从会话树中恢复。",
"closeConfirmAction": "关闭窗格"
}, },
"tabBar": { "tabBar": {
"noTabs": "暂无终端会话" "noTabs": "暂无终端会话"

View File

@@ -285,6 +285,13 @@ export const useSessionManagerStore = create<SessionManagerStore>()(
shellKind: session.shellKind, 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 { try {
// Close existing session // Close existing session
await closeCliSession(terminalId, projectPath ?? undefined); await closeCliSession(terminalId, projectPath ?? undefined);
@@ -293,7 +300,7 @@ export const useSessionManagerStore = create<SessionManagerStore>()(
const result = await createCliSession( const result = await createCliSession(
{ {
workingDir: sessionConfig.workingDir, workingDir: sessionConfig.workingDir,
preferredShell: sessionConfig.shellKind === 'powershell' ? 'pwsh' : 'bash', preferredShell: mapShellKind(sessionConfig.shellKind),
tool: sessionConfig.tool, tool: sessionConfig.tool,
model: sessionConfig.model, model: sessionConfig.model,
resumeKey: sessionConfig.resumeKey, resumeKey: sessionConfig.resumeKey,
@@ -325,6 +332,35 @@ export const useSessionManagerStore = create<SessionManagerStore>()(
throw error; 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' } { name: 'SessionManagerStore' }
) )

View File

@@ -89,6 +89,8 @@ export interface SessionManagerActions {
resumeSession: (terminalId: string) => Promise<void>; resumeSession: (terminalId: string) => Promise<void>;
/** Restart a terminal session (close and recreate with same config) */ /** Restart a terminal session (close and recreate with same config) */
restartSession: (terminalId: string) => Promise<void>; restartSession: (terminalId: string) => Promise<void>;
/** Close and terminate a terminal session permanently */
closeSession: (terminalId: string) => Promise<void>;
} }
export type SessionManagerStore = SessionManagerState & SessionManagerActions; export type SessionManagerStore = SessionManagerState & SessionManagerActions;

View File

@@ -280,8 +280,10 @@ export class CliSessionManager {
} else { } else {
// Legacy shell session: spawn bash/pwsh // Legacy shell session: spawn bash/pwsh
const preferredShell = options.preferredShell ?? 'bash'; // Note: 'cmd' is for CLI tools only, for legacy shells we default to bash
const picked = pickShell(preferredShell); const shellPreference = options.preferredShell ?? 'bash';
const preferredShell = shellPreference === 'cmd' ? 'bash' : shellPreference;
const picked = pickShell(preferredShell as 'bash' | 'pwsh');
shellKind = picked.shellKind; shellKind = picked.shellKind;
file = picked.file; file = picked.file;
args = picked.args; args = picked.args;