import React, { useState, useEffect } from "react"; import { Button } from "@/components/ui/button.tsx"; import { Input } from "@/components/ui/input.tsx"; import { Textarea } from "@/components/ui/textarea.tsx"; import { Separator } from "@/components/ui/separator.tsx"; import { Checkbox } from "@/components/ui/checkbox.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select.tsx"; import { Label } from "@/components/ui/label.tsx"; import { Tabs, TabsList, TabsTrigger, TabsContent, } from "@/components/ui/tabs.tsx"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip.tsx"; import { Sidebar, SidebarContent, SidebarHeader, SidebarProvider, SidebarGroupLabel, } from "@/components/ui/sidebar.tsx"; import { Plus, Play, Edit, Trash2, Copy, X, RotateCcw, Search, Loader2, Terminal, LayoutGrid, MonitorCheck, Folder, ChevronDown, ChevronRight, GripVertical, FolderPlus, Settings, MoreVertical, Server, Cloud, Database, Box, Package, Layers, Archive, HardDrive, Globe, } from "lucide-react"; import { toast } from "sonner"; import { useTranslation } from "react-i18next"; import { useConfirmation } from "@/hooks/use-confirmation.ts"; import { getSnippets, createSnippet, updateSnippet, deleteSnippet, getCookie, setCookie, getCommandHistory, deleteCommandFromHistory, getSnippetFolders, createSnippetFolder, updateSnippetFolderMetadata, renameSnippetFolder, deleteSnippetFolder, reorderSnippets, } from "@/ui/main-axios.ts"; import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx"; import type { Snippet, SnippetData, SnippetFolder } from "../../../../types"; interface TabData { id: number; type: string; title: string; terminalRef?: { current?: { sendInput?: (data: string) => void; }; }; hostConfig?: { id: number; }; isActive?: boolean; [key: string]: unknown; } interface SSHToolsSidebarProps { isOpen: boolean; onClose: () => void; onSnippetExecute: (content: string) => void; sidebarWidth: number; setSidebarWidth: (width: number) => void; initialTab?: string; onTabChange?: () => void; } const AVAILABLE_COLORS = [ { value: "#ef4444", label: "Red" }, { value: "#f97316", label: "Orange" }, { value: "#eab308", label: "Yellow" }, { value: "#22c55e", label: "Green" }, { value: "#3b82f6", label: "Blue" }, { value: "#a855f7", label: "Purple" }, { value: "#ec4899", label: "Pink" }, { value: "#6b7280", label: "Gray" }, ]; const AVAILABLE_ICONS = [ { value: "Folder", label: "Folder", Icon: Folder }, { value: "Server", label: "Server", Icon: Server }, { value: "Cloud", label: "Cloud", Icon: Cloud }, { value: "Database", label: "Database", Icon: Database }, { value: "Box", label: "Box", Icon: Box }, { value: "Package", label: "Package", Icon: Package }, { value: "Layers", label: "Layers", Icon: Layers }, { value: "Archive", label: "Archive", Icon: Archive }, { value: "HardDrive", label: "HardDrive", Icon: HardDrive }, { value: "Globe", label: "Globe", Icon: Globe }, ]; export function SSHToolsSidebar({ isOpen, onClose, onSnippetExecute, sidebarWidth, setSidebarWidth, initialTab, onTabChange, }: SSHToolsSidebarProps) { const { t } = useTranslation(); const { confirmWithToast } = useConfirmation(); const { tabs, currentTab, allSplitScreenTab, setSplitScreenTab, setCurrentTab, } = useTabs() as { tabs: TabData[]; currentTab: number | null; allSplitScreenTab: number[]; setSplitScreenTab: (tabId: number) => void; setCurrentTab: (tabId: number) => void; }; const [activeTab, setActiveTab] = useState(initialTab || "ssh-tools"); useEffect(() => { if (initialTab && isOpen) { setActiveTab(initialTab); } }, [initialTab, isOpen]); const handleTabChange = (tab: string) => { setActiveTab(tab); if (onTabChange) { onTabChange(); } }; const [isRecording, setIsRecording] = useState(false); const [selectedTabIds, setSelectedTabIds] = useState([]); const [rightClickCopyPaste, setRightClickCopyPaste] = useState( () => getCookie("rightClickCopyPaste") === "true", ); const [snippets, setSnippets] = useState([]); const [snippetFolders, setSnippetFolders] = useState([]); const [loading, setLoading] = useState(true); const [showDialog, setShowDialog] = useState(false); const [editingSnippet, setEditingSnippet] = useState(null); const [formData, setFormData] = useState({ name: "", content: "", description: "", }); const [formErrors, setFormErrors] = useState({ name: false, content: false, }); const [selectedSnippetTabIds, setSelectedSnippetTabIds] = useState( [], ); const [draggedSnippet, setDraggedSnippet] = useState(null); const [dragOverFolder, setDragOverFolder] = useState(null); const [collapsedFolders, setCollapsedFolders] = useState>(() => { const shouldCollapse = localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false"; return shouldCollapse ? new Set() : new Set(); }); const [showFolderDialog, setShowFolderDialog] = useState(false); const [editingFolder, setEditingFolder] = useState( null, ); const [folderFormData, setFolderFormData] = useState({ name: "", color: "", icon: "", }); const [folderFormErrors, setFolderFormErrors] = useState({ name: false, }); const [commandHistory, setCommandHistory] = useState([]); const [isHistoryLoading, setIsHistoryLoading] = useState(false); const [historyError, setHistoryError] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [historyRefreshCounter, setHistoryRefreshCounter] = useState(0); const commandHistoryScrollRef = React.useRef(null); const [splitMode, setSplitMode] = useState<"none" | "2" | "3" | "4">("none"); const [splitAssignments, setSplitAssignments] = useState>( new Map(), ); const [previewKey, setPreviewKey] = useState(0); const [draggedTabId, setDraggedTabId] = useState(null); const [dragOverCellIndex, setDragOverCellIndex] = useState( null, ); const [isResizing, setIsResizing] = useState(false); const startXRef = React.useRef(null); const startWidthRef = React.useRef(sidebarWidth); const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal"); const activeUiTab = tabs.find((tab) => tab.id === currentTab); const activeTerminal = activeUiTab?.type === "terminal" ? activeUiTab : undefined; const activeTerminalHostId = activeTerminal?.hostConfig?.id; const splittableTabs = tabs.filter( (tab: TabData) => tab.type === "terminal" || tab.type === "server" || tab.type === "file_manager" || tab.type === "user_profile", ); useEffect(() => { let cancelled = false; if (isOpen && activeTab === "command-history") { if (activeTerminalHostId) { const scrollTop = commandHistoryScrollRef.current?.scrollTop || 0; setIsHistoryLoading(true); setHistoryError(null); getCommandHistory(activeTerminalHostId) .then((history) => { if (cancelled) return; setCommandHistory((prevHistory) => { const newHistory = Array.isArray(history) ? history : []; if (JSON.stringify(prevHistory) !== JSON.stringify(newHistory)) { requestAnimationFrame(() => { if (commandHistoryScrollRef.current) { commandHistoryScrollRef.current.scrollTop = scrollTop; } }); return newHistory; } return prevHistory; }); setIsHistoryLoading(false); }) .catch((err) => { if (cancelled) return; console.error("Failed to fetch command history", err); const errorMessage = err?.response?.status === 401 ? t("commandHistory.authRequiredRefresh") : err?.response?.status === 403 ? t("commandHistory.dataAccessLockedReauth") : err?.message || "Failed to load command history"; setHistoryError(errorMessage); setCommandHistory([]); setIsHistoryLoading(false); }); } else { setCommandHistory([]); setHistoryError(null); setIsHistoryLoading(false); } } return () => { cancelled = true; }; }, [ isOpen, activeTab, activeTerminalHostId, currentTab, historyRefreshCounter, ]); useEffect(() => { if (isOpen && activeTab === "command-history" && activeTerminalHostId) { const refreshInterval = setInterval(() => { setHistoryRefreshCounter((prev) => prev + 1); }, 2000); return () => clearInterval(refreshInterval); } }, [isOpen, activeTab, activeTerminalHostId]); const filteredCommands = searchQuery ? commandHistory.filter((cmd) => cmd.toLowerCase().includes(searchQuery.toLowerCase()), ) : commandHistory; useEffect(() => { document.documentElement.style.setProperty( "--right-sidebar-width", `${sidebarWidth}px`, ); }, [sidebarWidth]); useEffect(() => { const handleResize = () => { const minWidth = Math.min(300, Math.floor(window.innerWidth * 0.2)); const maxWidth = Math.floor(window.innerWidth * 0.3); if (sidebarWidth > maxWidth) { setSidebarWidth(Math.max(minWidth, maxWidth)); } else if (sidebarWidth < minWidth) { setSidebarWidth(minWidth); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [sidebarWidth, setSidebarWidth]); useEffect(() => { if (isOpen && activeTab === "snippets") { fetchSnippets(); } }, [isOpen, activeTab]); useEffect(() => { if (snippetFolders.length > 0) { const shouldCollapse = localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false"; if (shouldCollapse) { const allFolderNames = new Set(snippetFolders.map((f) => f.name)); const uncategorizedSnippets = snippets.filter( (s) => !s.folder || s.folder === "", ); if (uncategorizedSnippets.length > 0) { allFolderNames.add(""); } setCollapsedFolders(allFolderNames); } else { setCollapsedFolders(new Set()); } } }, [snippetFolders, snippets]); useEffect(() => { const handleSettingChange = () => { const shouldCollapse = localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false"; if (shouldCollapse) { const allFolderNames = new Set(snippetFolders.map((f) => f.name)); const uncategorizedSnippets = snippets.filter( (s) => !s.folder || s.folder === "", ); if (uncategorizedSnippets.length > 0) { allFolderNames.add(""); } setCollapsedFolders(allFolderNames); } else { setCollapsedFolders(new Set()); } }; window.addEventListener( "defaultSnippetFoldersCollapsedChanged", handleSettingChange, ); return () => { window.removeEventListener( "defaultSnippetFoldersCollapsedChanged", handleSettingChange, ); }; }, [snippetFolders, snippets]); const handleMouseDown = (e: React.MouseEvent) => { e.preventDefault(); setIsResizing(true); startXRef.current = e.clientX; startWidthRef.current = sidebarWidth; }; React.useEffect(() => { if (!isResizing) return; const handleMouseMove = (e: MouseEvent) => { if (startXRef.current == null) return; const dx = startXRef.current - e.clientX; const newWidth = Math.round(startWidthRef.current + dx); const minWidth = Math.min(300, Math.floor(window.innerWidth * 0.2)); const maxWidth = Math.round(window.innerWidth * 0.3); let finalWidth = newWidth; if (newWidth < minWidth) { finalWidth = minWidth; } else if (newWidth > maxWidth) { finalWidth = maxWidth; } document.documentElement.style.setProperty( "--right-sidebar-width", `${finalWidth}px`, ); setSidebarWidth(finalWidth); }; const handleMouseUp = () => { setIsResizing(false); startXRef.current = null; }; document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); document.body.style.cursor = "col-resize"; document.body.style.userSelect = "none"; return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); document.body.style.cursor = ""; document.body.style.userSelect = ""; }; }, [isResizing]); const handleTabToggle = (tabId: number) => { setSelectedTabIds((prev) => prev.includes(tabId) ? prev.filter((id) => id !== tabId) : [...prev, tabId], ); }; const handleStartRecording = () => { setIsRecording(true); setTimeout(() => { const input = document.getElementById( "ssh-tools-input", ) as HTMLInputElement; if (input) input.focus(); }, 100); }; const handleStopRecording = () => { setIsRecording(false); setSelectedTabIds([]); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (selectedTabIds.length === 0) return; let commandToSend = ""; if (e.ctrlKey || e.metaKey) { if (e.key === "c") { commandToSend = "\x03"; e.preventDefault(); } else if (e.key === "d") { commandToSend = "\x04"; e.preventDefault(); } else if (e.key === "l") { commandToSend = "\x0c"; e.preventDefault(); } else if (e.key === "u") { commandToSend = "\x15"; e.preventDefault(); } else if (e.key === "k") { commandToSend = "\x0b"; e.preventDefault(); } else if (e.key === "a") { commandToSend = "\x01"; e.preventDefault(); } else if (e.key === "e") { commandToSend = "\x05"; e.preventDefault(); } else if (e.key === "w") { commandToSend = "\x17"; e.preventDefault(); } } else if (e.key === "Enter") { commandToSend = "\n"; e.preventDefault(); } else if (e.key === "Backspace") { commandToSend = "\x08"; e.preventDefault(); } else if (e.key === "Delete") { commandToSend = "\x7f"; e.preventDefault(); } else if (e.key === "Tab") { commandToSend = "\x09"; e.preventDefault(); } else if (e.key === "Escape") { commandToSend = "\x1b"; e.preventDefault(); } else if (e.key === "ArrowUp") { commandToSend = "\x1b[A"; e.preventDefault(); } else if (e.key === "ArrowDown") { commandToSend = "\x1b[B"; e.preventDefault(); } else if (e.key === "ArrowLeft") { commandToSend = "\x1b[D"; e.preventDefault(); } else if (e.key === "ArrowRight") { commandToSend = "\x1b[C"; e.preventDefault(); } else if (e.key === "Home") { commandToSend = "\x1b[H"; e.preventDefault(); } else if (e.key === "End") { commandToSend = "\x1b[F"; e.preventDefault(); } else if (e.key === "PageUp") { commandToSend = "\x1b[5~"; e.preventDefault(); } else if (e.key === "PageDown") { commandToSend = "\x1b[6~"; e.preventDefault(); } else if (e.key === "Insert") { commandToSend = "\x1b[2~"; e.preventDefault(); } else if (e.key === "F1") { commandToSend = "\x1bOP"; e.preventDefault(); } else if (e.key === "F2") { commandToSend = "\x1bOQ"; e.preventDefault(); } else if (e.key === "F3") { commandToSend = "\x1bOR"; e.preventDefault(); } else if (e.key === "F4") { commandToSend = "\x1bOS"; e.preventDefault(); } else if (e.key === "F5") { commandToSend = "\x1b[15~"; e.preventDefault(); } else if (e.key === "F6") { commandToSend = "\x1b[17~"; e.preventDefault(); } else if (e.key === "F7") { commandToSend = "\x1b[18~"; e.preventDefault(); } else if (e.key === "F8") { commandToSend = "\x1b[19~"; e.preventDefault(); } else if (e.key === "F9") { commandToSend = "\x1b[20~"; e.preventDefault(); } else if (e.key === "F10") { commandToSend = "\x1b[21~"; e.preventDefault(); } else if (e.key === "F11") { commandToSend = "\x1b[23~"; e.preventDefault(); } else if (e.key === "F12") { commandToSend = "\x1b[24~"; e.preventDefault(); } if (commandToSend) { selectedTabIds.forEach((tabId) => { const tab = tabs.find((t: TabData) => t.id === tabId); if (tab?.terminalRef?.current?.sendInput) { tab.terminalRef.current.sendInput(commandToSend); } }); } }; const handleKeyPress = (e: React.KeyboardEvent) => { if (selectedTabIds.length === 0) return; if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) { const char = e.key; selectedTabIds.forEach((tabId) => { const tab = tabs.find((t: TabData) => t.id === tabId); if (tab?.terminalRef?.current?.sendInput) { tab.terminalRef.current.sendInput(char); } }); } }; const updateRightClickCopyPaste = (checked: boolean) => { setCookie("rightClickCopyPaste", checked.toString()); setRightClickCopyPaste(checked); }; const fetchSnippets = async () => { try { setLoading(true); const [snippetsData, foldersData] = await Promise.all([ getSnippets(), getSnippetFolders(), ]); setSnippets(Array.isArray(snippetsData) ? snippetsData : []); setSnippetFolders(Array.isArray(foldersData) ? foldersData : []); } catch { toast.error(t("snippets.failedToFetch")); setSnippets([]); setSnippetFolders([]); } finally { setLoading(false); } }; const handleCreate = () => { setEditingSnippet(null); setFormData({ name: "", content: "", description: "" }); setFormErrors({ name: false, content: false }); setShowDialog(true); }; const handleEdit = (snippet: Snippet) => { setEditingSnippet(snippet); setFormData({ name: snippet.name, content: snippet.content, description: snippet.description || "", folder: snippet.folder, }); setFormErrors({ name: false, content: false }); setShowDialog(true); }; const handleDelete = (snippet: Snippet) => { confirmWithToast( t("snippets.deleteConfirmDescription", { name: snippet.name }), async () => { try { await deleteSnippet(snippet.id); toast.success(t("snippets.deleteSuccess")); fetchSnippets(); } catch { toast.error(t("snippets.deleteFailed")); } }, "destructive", ); }; const handleSubmit = async () => { const errors = { name: !formData.name.trim(), content: !formData.content.trim(), }; setFormErrors(errors); if (errors.name || errors.content) { return; } try { if (editingSnippet) { await updateSnippet(editingSnippet.id, formData); toast.success(t("snippets.updateSuccess")); } else { await createSnippet(formData); toast.success(t("snippets.createSuccess")); } setShowDialog(false); fetchSnippets(); } catch { toast.error( editingSnippet ? t("snippets.updateFailed") : t("snippets.createFailed"), ); } }; const handleSnippetTabToggle = (tabId: number) => { setSelectedSnippetTabIds((prev) => prev.includes(tabId) ? prev.filter((id) => id !== tabId) : [...prev, tabId], ); }; const handleExecute = (snippet: Snippet) => { if (selectedSnippetTabIds.length > 0) { selectedSnippetTabIds.forEach((tabId) => { const tab = tabs.find((t: TabData) => t.id === tabId); if (tab?.terminalRef?.current?.sendInput) { tab.terminalRef.current.sendInput(snippet.content + "\n"); } }); toast.success( t("snippets.executeSuccess", { name: snippet.name, count: selectedSnippetTabIds.length, }), ); } else { onSnippetExecute(snippet.content); toast.success(t("snippets.executeSuccess", { name: snippet.name })); } // Remove focus from any active element in the sidebar to prevent accidental re-execution if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } }; const handleCopy = (snippet: Snippet) => { navigator.clipboard.writeText(snippet.content); toast.success(t("snippets.copySuccess", { name: snippet.name })); }; const toggleFolder = (folderName: string) => { setCollapsedFolders((prev) => { const next = new Set(prev); if (next.has(folderName)) { next.delete(folderName); } else { next.add(folderName); } return next; }); }; const getFolderIcon = (folderName: string) => { const metadata = snippetFolders.find((f) => f.name === folderName); if (!metadata?.icon) return Folder; const iconData = AVAILABLE_ICONS.find((i) => i.value === metadata.icon); return iconData?.Icon || Folder; }; const getFolderColor = (folderName: string) => { const metadata = snippetFolders.find((f) => f.name === folderName); return metadata?.color; }; const groupSnippetsByFolder = () => { const grouped = new Map(); snippetFolders.forEach((folder) => { if (!grouped.has(folder.name)) { grouped.set(folder.name, []); } }); snippets.forEach((snippet) => { const folderName = snippet.folder || ""; if (!grouped.has(folderName)) { grouped.set(folderName, []); } grouped.get(folderName)!.push(snippet); }); return grouped; }; const handleDragStart = (e: React.DragEvent, snippet: Snippet) => { setDraggedSnippet(snippet); e.dataTransfer.effectAllowed = "move"; }; const handleDragOver = (e: React.DragEvent, targetSnippet: Snippet) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }; const handleDragEnterFolder = (folderName: string) => { setDragOverFolder(folderName); }; const handleDragLeaveFolder = () => { setDragOverFolder(null); }; const handleDrop = async (e: React.DragEvent, targetSnippet: Snippet) => { e.preventDefault(); if (!draggedSnippet || draggedSnippet.id === targetSnippet.id) { setDraggedSnippet(null); setDragOverFolder(null); return; } const sourceFolder = draggedSnippet.folder || ""; const targetFolder = targetSnippet.folder || ""; if (sourceFolder !== targetFolder) { toast.error( t("snippets.reorderSameFolder"), ); setDraggedSnippet(null); setDragOverFolder(null); return; } const folderSnippets = snippets.filter( (s) => (s.folder || "") === targetFolder, ); const draggedIndex = folderSnippets.findIndex( (s) => s.id === draggedSnippet.id, ); const targetIndex = folderSnippets.findIndex( (s) => s.id === targetSnippet.id, ); if (draggedIndex === -1 || targetIndex === -1) { setDraggedSnippet(null); setDragOverFolder(null); return; } const reorderedSnippets = [...folderSnippets]; reorderedSnippets.splice(draggedIndex, 1); reorderedSnippets.splice(targetIndex, 0, draggedSnippet); const updates = reorderedSnippets.map((snippet, index) => ({ id: snippet.id, order: index, folder: targetFolder || undefined, })); try { await reorderSnippets(updates); toast.success( t("snippets.reorderSuccess"), ); fetchSnippets(); } catch { toast.error( t("snippets.reorderFailed"), ); } setDraggedSnippet(null); setDragOverFolder(null); }; const handleDragEnd = () => { setDraggedSnippet(null); setDragOverFolder(null); }; const handleCreateFolder = () => { setEditingFolder(null); setFolderFormData({ name: "", color: AVAILABLE_COLORS[0].value, icon: AVAILABLE_ICONS[0].value, }); setFolderFormErrors({ name: false }); setShowFolderDialog(true); }; const handleEditFolder = (folder: SnippetFolder) => { setEditingFolder(folder); setFolderFormData({ name: folder.name, color: folder.color || AVAILABLE_COLORS[0].value, icon: folder.icon || AVAILABLE_ICONS[0].value, }); setFolderFormErrors({ name: false }); setShowFolderDialog(true); }; const handleDeleteFolder = (folderName: string) => { confirmWithToast( t("snippets.deleteFolderConfirm", { name: folderName, async () => { try { await deleteSnippetFolder(folderName); toast.success(t("snippets.deleteFolderSuccess")); fetchSnippets(); } catch { toast.error(t("snippets.deleteFolderFailed")); } }, "destructive", ); }; const handleFolderSubmit = async () => { const errors = { name: !folderFormData.name.trim(), }; setFolderFormErrors(errors); if (errors.name) { return; } try { if (editingFolder) { if (editingFolder.name !== folderFormData.name) { await renameSnippetFolder(editingFolder.name, folderFormData.name); } await updateSnippetFolderMetadata(folderFormData.name, { color: folderFormData.color || undefined, icon: folderFormData.icon || undefined, }); toast.success(t("snippets.updateFolderSuccess")); } else { await createSnippetFolder({ name: folderFormData.name, color: folderFormData.color || undefined, icon: folderFormData.icon || undefined, }); toast.success(t("snippets.createFolderSuccess")); } setShowFolderDialog(false); fetchSnippets(); } catch { toast.error( editingFolder ? t("snippets.updateFolderFailed") : t("snippets.createFolderFailed"), ); } }; const handleSplitModeChange = (mode: "none" | "2" | "3" | "4") => { setSplitMode(mode); if (mode === "none") { handleClearSplit(); } else { setSplitAssignments(new Map()); setPreviewKey((prev) => prev + 1); } }; const handleTabDragStart = (tabId: number) => { setDraggedTabId(tabId); }; const handleTabDragEnd = () => { setDraggedTabId(null); setDragOverCellIndex(null); }; const handleTabDragOver = (e: React.DragEvent, cellIndex: number) => { e.preventDefault(); setDragOverCellIndex(cellIndex); }; const handleTabDragLeave = () => { setDragOverCellIndex(null); }; const handleTabDrop = (cellIndex: number) => { if (draggedTabId === null) return; setSplitAssignments((prev) => { const newMap = new Map(prev); Array.from(newMap.entries()).forEach(([idx, id]) => { if (id === draggedTabId && idx !== cellIndex) { newMap.delete(idx); } }); newMap.set(cellIndex, draggedTabId); return newMap; }); setDraggedTabId(null); setDragOverCellIndex(null); setPreviewKey((prev) => prev + 1); }; const handleRemoveFromCell = (cellIndex: number) => { setSplitAssignments((prev) => { const newMap = new Map(prev); newMap.delete(cellIndex); setPreviewKey((prev) => prev + 1); return newMap; }); }; const handleApplySplit = () => { if (splitMode === "none") { handleClearSplit(); return; } if (splitAssignments.size === 0) { toast.error( t("splitScreen.error.noAssignments"), ); return; } const requiredSlots = parseInt(splitMode); if (splitAssignments.size < requiredSlots) { toast.error( t("splitScreen.error.fillAllSlots", { count: requiredSlots, }), ); return; } const orderedTabIds: number[] = []; for (let i = 0; i < requiredSlots; i++) { const tabId = splitAssignments.get(i); if (tabId !== undefined) { orderedTabIds.push(tabId); } } const currentSplits = [...allSplitScreenTab]; currentSplits.forEach((tabId) => { setSplitScreenTab(tabId); }); orderedTabIds.forEach((tabId) => { setSplitScreenTab(tabId); }); if (!orderedTabIds.includes(currentTab ?? 0)) { setCurrentTab(orderedTabIds[0]); } toast.success( t("splitScreen.success"), ); }; const handleClearSplit = () => { allSplitScreenTab.forEach((tabId) => { setSplitScreenTab(tabId); }); setSplitMode("none"); setSplitAssignments(new Map()); setPreviewKey((prev) => prev + 1); toast.success( t("splitScreen.cleared"), ); }; const handleResetToSingle = () => { handleClearSplit(); }; const handleCommandSelect = (command: string) => { if (activeTerminal?.terminalRef?.current?.sendInput) { activeTerminal.terminalRef.current.sendInput(command); } }; const handleCommandDelete = async (command: string) => { if (activeTerminalHostId) { try { await deleteCommandFromHistory(activeTerminalHostId, command); setCommandHistory((prev) => prev.filter((c) => c !== command)); toast.success( t("commandHistory.deleteSuccess"), ); } catch { toast.error( t("commandHistory.deleteFailed"), ); } } }; return ( <> {isOpen && (
{t("nav.tools")}
{t("sshTools.title")} {t("snippets.title")} {t("commandHistory.title")} {t("splitScreen.title")}

{t("sshTools.keyRecording")}

{!isRecording ? ( ) : ( )}
{isRecording && ( <>
{terminalTabs.map((tab) => ( ))}

{t("sshTools.commandsWillBeSent", { count: selectedTabIds.length, })}

)}

{t("sshTools.settings")}

{terminalTabs.length > 0 && ( <>

{selectedSnippetTabIds.length > 0 ? t("snippets.executeOnSelected", { count: selectedSnippetTabIds.length, }) : t("snippets.executeOnCurrent")}

{terminalTabs.map((tab) => ( ))}
)}
{loading ? (

{t("common.loading")}

) : snippets.length === 0 && snippetFolders.length === 0 ? (

{t("snippets.empty")}

{t("snippets.emptyHint")}

) : (
{Array.from(groupSnippetsByFolder()).map( ([folderName, folderSnippets]) => { const folderMetadata = snippetFolders.find( (f) => f.name === folderName, ); const isCollapsed = collapsedFolders.has(folderName); return (
toggleFolder(folderName)} > {isCollapsed ? ( ) : ( )} {(() => { const FolderIcon = getFolderIcon(folderName); const folderColor = getFolderColor(folderName); return ( ); })()} {folderName || t("snippets.uncategorized", { defaultValue: "Uncategorized", })} {folderSnippets.length}
{folderName && (
)}
{!isCollapsed && (
{folderSnippets.map((snippet) => (
handleDragStart(e, snippet) } onDragOver={(e) => handleDragOver(e, snippet) } onDrop={(e) => handleDrop(e, snippet)} onDragEnd={handleDragEnd} className={`bg-dark-bg-input border border-input rounded-lg cursor-move hover:shadow-lg hover:border-gray-400/50 hover:bg-dark-hover-alt transition-all duration-200 p-3 group ${ draggedSnippet?.id === snippet.id ? "opacity-50" : "" }`} >

{snippet.name}

{snippet.description && (

{snippet.description}

)}

ID: {snippet.id}

{snippet.content}

{t("snippets.runTooltip")}

{t("snippets.copyTooltip")}

{t("snippets.editTooltip")}

{t("snippets.deleteTooltip")}

))}
)}
); }, )}
)}
{ setSearchQuery(e.target.value); }} className="pl-10 pr-10" /> {searchQuery && ( )}

{t("commandHistory.tabHint")}

{historyError ? (

{t("commandHistory.error")}

{historyError}

) : !activeTerminal ? (

{t("commandHistory.noTerminal")}

{t("commandHistory.noTerminalHint")}

) : isHistoryLoading && commandHistory.length === 0 ? (

{t("commandHistory.loading")}

) : filteredCommands.length === 0 ? (
{searchQuery ? ( <>

{t("commandHistory.noResults")}

{t("commandHistory.noResultsHint", { query: searchQuery, })}

) : ( <>

{t("commandHistory.empty")}

{t("commandHistory.emptyHint")}

)}
) : (
{filteredCommands.map((command, index) => (
handleCommandSelect(command)} title={command} > {command}
))}
)}
handleSplitModeChange( value as "none" | "2" | "3" | "4", ) } className="w-full" > {t("splitScreen.none")} {t("splitScreen.twoSplit")} {t("splitScreen.threeSplit")} {t("splitScreen.fourSplit")} {splitMode !== "none" && ( <>

{t("splitScreen.dragTabsHint")}

{splittableTabs.map((tab) => { const isAssigned = Array.from( splitAssignments.values(), ).includes(tab.id); const isDragging = draggedTabId === tab.id; return (
handleTabDragStart(tab.id) } onDragEnd={handleTabDragEnd} className={` px-3 py-2 rounded-md text-sm cursor-move transition-all ${ isAssigned ? "bg-dark-bg/50 text-muted-foreground cursor-not-allowed opacity-50" : "bg-dark-bg border border-dark-border hover:border-gray-400 hover:bg-dark-bg-input" } ${isDragging ? "opacity-50" : ""} `} > {tab.title}
); })}
{Array.from( { length: parseInt(splitMode) }, (_, idx) => { const assignedTabId = splitAssignments.get(idx); const assignedTab = assignedTabId ? tabs.find((t) => t.id === assignedTabId) : null; const isHovered = dragOverCellIndex === idx; const isEmpty = !assignedTabId; return (
handleTabDragOver(e, idx) } onDragLeave={handleTabDragLeave} onDrop={() => handleTabDrop(idx)} className={` relative bg-dark-bg border-2 rounded-md p-3 min-h-[100px] flex flex-col items-center justify-center transition-all ${splitMode === "3" && idx === 2 ? "col-span-2" : ""} ${ isEmpty ? "border-dashed border-dark-border" : "border-solid border-gray-400 bg-gray-500/10" } ${ isHovered && draggedTabId ? "border-gray-500 bg-gray-500/20 ring-2 ring-gray-500/50" : "" } `} > {assignedTab ? ( <> {assignedTab.title} ) : ( {t("splitScreen.dropHere")} )}
); }, )}
)} {splitMode === "none" && (

{t("splitScreen.selectMode")}

{t("splitScreen.helpText")}

)}
{isOpen && (
{ if (!isResizing) { e.currentTarget.style.backgroundColor = "var(--dark-border-hover)"; } }} onMouseLeave={(e) => { if (!isResizing) { e.currentTarget.style.backgroundColor = "transparent"; } }} title={t("common.dragToResizeSidebar")} /> )}
)} {showDialog && (
setShowDialog(false)} >
e.stopPropagation()} >

{editingSnippet ? t("snippets.edit") : t("snippets.create")}

{editingSnippet ? t("snippets.editDescription") : t("snippets.createDescription")}

setFormData({ ...formData, name: e.target.value }) } placeholder={t("snippets.namePlaceholder")} className={`${formErrors.name ? "border-destructive focus-visible:ring-destructive" : ""}`} autoFocus /> {formErrors.name && (

{t("snippets.nameRequired")}

)}
setFormData({ ...formData, description: e.target.value }) } placeholder={t("snippets.descriptionPlaceholder")} />