mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: 添加会话关闭功能及确认对话框,更新相关国际化文本
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -227,6 +227,7 @@
|
|||||||
"turns": "回合",
|
"turns": "回合",
|
||||||
"perTurnView": "分回合视图",
|
"perTurnView": "分回合视图",
|
||||||
"concatenatedView": "连接视图",
|
"concatenatedView": "连接视图",
|
||||||
|
"nativeView": "原生视图",
|
||||||
"userPrompt": "用户提示词",
|
"userPrompt": "用户提示词",
|
||||||
"assistantResponse": "助手响应",
|
"assistantResponse": "助手响应",
|
||||||
"errors": "错误",
|
"errors": "错误",
|
||||||
|
|||||||
@@ -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": "暂无终端会话"
|
||||||
|
|||||||
@@ -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' }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user