Files
Termix/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx
T

2241 lines
83 KiB
TypeScript
Raw Normal View History

2025-11-17 09:46:05 -06:00
import React, { useState, useEffect } from "react";
+7
2025-11-05 10:36:16 -06:00
import { Button } from "@/components/ui/button.tsx";
import { Input } from "@/components/ui/input.tsx";
2025-11-17 09:46:05 -06:00
import { Textarea } from "@/components/ui/textarea.tsx";
+7
2025-11-05 10:36:16 -06:00
import { Separator } from "@/components/ui/separator.tsx";
2025-11-17 09:46:05 -06:00
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";
+7
2025-11-05 10:36:16 -06:00
import { useTranslation } from "react-i18next";
2025-11-17 09:46:05 -06:00
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";
+7
2025-11-05 10:36:16 -06:00
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
2025-11-17 09:46:05 -06:00
import type { Snippet, SnippetData, SnippetFolder } from "../../../../types";
+7
2025-11-05 10:36:16 -06:00
interface TabData {
id: number;
type: string;
title: string;
terminalRef?: {
current?: {
sendInput?: (data: string) => void;
};
};
2025-11-17 09:46:05 -06:00
hostConfig?: {
id: number;
};
isActive?: boolean;
+7
2025-11-05 10:36:16 -06:00
[key: string]: unknown;
}
2025-11-17 09:46:05 -06:00
interface SSHToolsSidebarProps {
+7
2025-11-05 10:36:16 -06:00
isOpen: boolean;
onClose: () => void;
2025-11-17 09:46:05 -06:00
onSnippetExecute: (content: string) => void;
sidebarWidth: number;
setSidebarWidth: (width: number) => void;
initialTab?: string;
onTabChange?: () => void;
+7
2025-11-05 10:36:16 -06:00
}
2025-11-17 09:46:05 -06:00
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 },
];
+7
2025-11-05 10:36:16 -06:00
export function SSHToolsSidebar({
isOpen,
onClose,
2025-11-17 09:46:05 -06:00
onSnippetExecute,
sidebarWidth,
setSidebarWidth,
initialTab,
onTabChange,
}: SSHToolsSidebarProps) {
+7
2025-11-05 10:36:16 -06:00
const { t } = useTranslation();
2025-11-17 09:46:05 -06:00
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();
}
};
+7
2025-11-05 10:36:16 -06:00
const [isRecording, setIsRecording] = useState(false);
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
2025-11-17 09:46:05 -06:00
const [rightClickCopyPaste, setRightClickCopyPaste] = useState<boolean>(
() => getCookie("rightClickCopyPaste") === "true",
);
const [snippets, setSnippets] = useState<Snippet[]>([]);
const [snippetFolders, setSnippetFolders] = useState<SnippetFolder[]>([]);
const [loading, setLoading] = useState(true);
const [showDialog, setShowDialog] = useState(false);
const [editingSnippet, setEditingSnippet] = useState<Snippet | null>(null);
const [formData, setFormData] = useState<SnippetData>({
name: "",
content: "",
description: "",
});
const [formErrors, setFormErrors] = useState({
name: false,
content: false,
});
const [selectedSnippetTabIds, setSelectedSnippetTabIds] = useState<number[]>(
[],
);
const [draggedSnippet, setDraggedSnippet] = useState<Snippet | null>(null);
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
+1
2025-12-31 22:20:12 -06:00
const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(() => {
const shouldCollapse =
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false";
return shouldCollapse ? new Set() : new Set();
});
const [snippetSearchQuery, setSnippetSearchQuery] = useState("");
2025-11-17 09:46:05 -06:00
const [showFolderDialog, setShowFolderDialog] = useState(false);
const [editingFolder, setEditingFolder] = useState<SnippetFolder | null>(
null,
);
const [folderFormData, setFolderFormData] = useState({
name: "",
color: "",
icon: "",
});
const [folderFormErrors, setFolderFormErrors] = useState({
name: false,
});
const [commandHistory, setCommandHistory] = useState<string[]>([]);
const [isHistoryLoading, setIsHistoryLoading] = useState(false);
const [historyError, setHistoryError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [historyRefreshCounter, setHistoryRefreshCounter] = useState(0);
const commandHistoryScrollRef = React.useRef<HTMLDivElement>(null);
+5
2026-03-08 18:02:14 -05:00
const [splitMode, setSplitMode] = useState<
"none" | "2" | "3" | "4" | "5" | "6"
>("none");
2025-11-17 09:46:05 -06:00
const [splitAssignments, setSplitAssignments] = useState<Map<number, number>>(
new Map(),
);
const [previewKey, setPreviewKey] = useState(0);
const [draggedTabId, setDraggedTabId] = useState<number | null>(null);
const [dragOverCellIndex, setDragOverCellIndex] = useState<number | null>(
null,
);
const [isResizing, setIsResizing] = useState(false);
const startXRef = React.useRef<number | null>(null);
const startWidthRef = React.useRef<number>(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" ||
+1
2025-12-31 22:20:12 -06:00
tab.type === "server_stats" ||
2025-11-17 09:46:05 -06:00
tab.type === "file_manager" ||
+1
2025-12-31 22:20:12 -06:00
tab.type === "tunnel" ||
tab.type === "docker",
2025-11-17 09:46:05 -06:00
);
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
+1
2025-12-31 22:20:12 -06:00
? t("commandHistory.authRequiredRefresh")
2025-11-17 09:46:05 -06:00
: err?.response?.status === 403
+1
2025-12-31 22:20:12 -06:00
? t("commandHistory.dataAccessLockedReauth")
2025-11-17 09:46:05 -06:00
: 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]);
+1
2025-12-31 22:20:12 -06:00
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]);
2025-11-17 09:46:05 -06:00
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]);
+7
2025-11-05 10:36:16 -06:00
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<HTMLInputElement>) => {
if (selectedTabIds.length === 0) return;
let commandToSend = "";
if (e.ctrlKey || e.metaKey) {
if (e.key === "c") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x03";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "d") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x04";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "l") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x0c";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "u") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x15";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "k") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x0b";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "a") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x01";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "e") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x05";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "w") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x17";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
}
} else if (e.key === "Enter") {
commandToSend = "\n";
e.preventDefault();
} else if (e.key === "Backspace") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x08";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "Delete") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x7f";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "Tab") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x09";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "Escape") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "ArrowUp") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[A";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "ArrowDown") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[B";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "ArrowLeft") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[D";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "ArrowRight") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[C";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "Home") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[H";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "End") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[F";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "PageUp") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[5~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "PageDown") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[6~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "Insert") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[2~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F1") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1bOP";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F2") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1bOQ";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F3") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1bOR";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F4") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1bOS";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F5") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[15~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F6") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[17~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F7") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[18~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F8") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[19~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F9") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[20~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F10") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[21~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F11") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[23~";
+7
2025-11-05 10:36:16 -06:00
e.preventDefault();
} else if (e.key === "F12") {
2025-11-17 09:46:05 -06:00
commandToSend = "\x1b[24~";
+7
2025-11-05 10:36:16 -06:00
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<HTMLInputElement>) => {
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());
2025-11-17 09:46:05 -06:00
setRightClickCopyPaste(checked);
+7
2025-11-05 10:36:16 -06:00
};
2025-11-17 09:46:05 -06:00
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);
}
};
+7
2025-11-05 10:36:16 -06:00
2025-11-17 09:46:05 -06:00
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],
);
};
+4
2026-02-12 22:28:13 -06:00
const handleExecute = async (snippet: Snippet) => {
const confirmEnabled =
localStorage.getItem("confirmSnippetExecution") === "true";
if (confirmEnabled) {
const confirmed = await confirmWithToast(
t("snippets.confirmExecution", { name: snippet.name }),
undefined,
"default",
t("common.cancel"),
{ confirmOnEnter: true, duration: 8000 },
);
if (!confirmed) {
return;
}
}
2025-11-17 09:46:05 -06:00
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 }));
}
+1
2025-12-31 22:20:12 -06:00
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
2025-11-17 09:46:05 -06:00
};
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<string, Snippet[]>();
snippetFolders.forEach((folder) => {
if (!grouped.has(folder.name)) {
grouped.set(folder.name, []);
}
});
+1
2025-12-31 22:20:12 -06:00
const filteredSnippets = snippetSearchQuery
? snippets.filter(
(snippet) =>
snippet.name
.toLowerCase()
.includes(snippetSearchQuery.toLowerCase()) ||
snippet.content
.toLowerCase()
.includes(snippetSearchQuery.toLowerCase()) ||
snippet.description
?.toLowerCase()
.includes(snippetSearchQuery.toLowerCase()),
)
: snippets;
filteredSnippets.forEach((snippet) => {
2025-11-17 09:46:05 -06:00
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) {
+1
2025-12-31 22:20:12 -06:00
toast.error(t("snippets.reorderSameFolder"));
2025-11-17 09:46:05 -06:00
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);
+1
2025-12-31 22:20:12 -06:00
toast.success(t("snippets.reorderSuccess"));
2025-11-17 09:46:05 -06:00
fetchSnippets();
} catch {
+1
2025-12-31 22:20:12 -06:00
toast.error(t("snippets.reorderFailed"));
2025-11-17 09:46:05 -06:00
}
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);
+1
2025-12-31 22:20:12 -06:00
toast.success(t("snippets.deleteFolderSuccess"));
2025-11-17 09:46:05 -06:00
fetchSnippets();
} catch {
+1
2025-12-31 22:20:12 -06:00
toast.error(t("snippets.deleteFolderFailed"));
2025-11-17 09:46:05 -06:00
}
},
"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,
});
+1
2025-12-31 22:20:12 -06:00
toast.success(t("snippets.updateFolderSuccess"));
2025-11-17 09:46:05 -06:00
} else {
await createSnippetFolder({
name: folderFormData.name,
color: folderFormData.color || undefined,
icon: folderFormData.icon || undefined,
});
+1
2025-12-31 22:20:12 -06:00
toast.success(t("snippets.createFolderSuccess"));
2025-11-17 09:46:05 -06:00
}
setShowFolderDialog(false);
fetchSnippets();
} catch {
toast.error(
editingFolder
+1
2025-12-31 22:20:12 -06:00
? t("snippets.updateFolderFailed")
: t("snippets.createFolderFailed"),
2025-11-17 09:46:05 -06:00
);
}
};
+5
2026-03-08 18:02:14 -05:00
const handleSplitModeChange = (
mode: "none" | "2" | "3" | "4" | "5" | "6",
) => {
2025-11-17 09:46:05 -06:00
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) {
+1
2025-12-31 22:20:12 -06:00
toast.error(t("splitScreen.error.noAssignments"));
2025-11-17 09:46:05 -06:00
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]);
}
+1
2025-12-31 22:20:12 -06:00
toast.success(t("splitScreen.success"));
2025-11-17 09:46:05 -06:00
};
const handleClearSplit = () => {
allSplitScreenTab.forEach((tabId) => {
setSplitScreenTab(tabId);
});
setSplitMode("none");
setSplitAssignments(new Map());
setPreviewKey((prev) => prev + 1);
+1
2025-12-31 22:20:12 -06:00
toast.success(t("splitScreen.cleared"));
2025-11-17 09:46:05 -06:00
};
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));
+1
2025-12-31 22:20:12 -06:00
toast.success(t("commandHistory.deleteSuccess"));
2025-11-17 09:46:05 -06:00
} catch {
+1
2025-12-31 22:20:12 -06:00
toast.error(t("commandHistory.deleteFailed"));
2025-11-17 09:46:05 -06:00
}
}
};
+7
2025-11-05 10:36:16 -06:00
return (
2025-11-17 09:46:05 -06:00
<>
{isOpen && (
<div className="fixed top-0 right-0 h-0 w-0 pointer-events-none">
<SidebarProvider
open={isOpen}
style={
{ "--sidebar-width": `${sidebarWidth}px` } as React.CSSProperties
}
className="!min-h-0 !h-0 !w-0"
+7
2025-11-05 10:36:16 -06:00
>
2025-11-17 09:46:05 -06:00
<Sidebar
variant="floating"
side="right"
className="pointer-events-auto"
>
<SidebarHeader>
+1
2025-12-31 22:20:12 -06:00
<SidebarGroupLabel className="text-lg font-bold text-foreground">
2025-11-17 09:46:05 -06:00
{t("nav.tools")}
<div className="absolute right-5 flex gap-1">
+7
2025-11-05 10:36:16 -06:00
<Button
variant="outline"
2025-11-17 09:46:05 -06:00
onClick={() => setSidebarWidth(400)}
className="w-[28px] h-[28px]"
+1
2025-12-31 22:20:12 -06:00
title={t("common.resetSidebarWidth")}
+7
2025-11-05 10:36:16 -06:00
>
2025-11-17 09:46:05 -06:00
<RotateCcw className="h-4 w-4" />
+7
2025-11-05 10:36:16 -06:00
</Button>
<Button
2025-11-17 09:46:05 -06:00
variant="outline"
onClick={onClose}
className="w-[28px] h-[28px]"
title={t("common.close")}
+7
2025-11-05 10:36:16 -06:00
>
2025-11-17 09:46:05 -06:00
<X className="h-4 w-4" />
+7
2025-11-05 10:36:16 -06:00
</Button>
2025-11-17 09:46:05 -06:00
</div>
</SidebarGroupLabel>
</SidebarHeader>
<Separator className="p-0.25" />
<SidebarContent className="p-4 flex flex-col overflow-hidden">
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="flex flex-col h-full overflow-hidden"
>
<TabsList className="w-full grid grid-cols-4 mb-4 flex-shrink-0">
<TabsTrigger value="ssh-tools">
{t("sshTools.title")}
</TabsTrigger>
<TabsTrigger value="snippets">
{t("snippets.title")}
</TabsTrigger>
<TabsTrigger value="command-history">
+1
2025-12-31 22:20:12 -06:00
{t("commandHistory.title")}
2025-11-17 09:46:05 -06:00
</TabsTrigger>
<TabsTrigger value="split-screen">
+1
2025-12-31 22:20:12 -06:00
{t("splitScreen.title")}
2025-11-17 09:46:05 -06:00
</TabsTrigger>
</TabsList>
+7
2025-11-05 10:36:16 -06:00
2025-11-17 09:46:05 -06:00
<TabsContent value="ssh-tools" className="space-y-4">
+1
2025-12-31 22:20:12 -06:00
<h3 className="font-semibold text-foreground">
2025-11-17 09:46:05 -06:00
{t("sshTools.keyRecording")}
</h3>
<div className="space-y-4">
<div className="flex gap-2">
{!isRecording ? (
+7
2025-11-05 10:36:16 -06:00
<Button
2025-11-17 09:46:05 -06:00
onClick={handleStartRecording}
className="flex-1"
+7
2025-11-05 10:36:16 -06:00
variant="outline"
>
2025-11-17 09:46:05 -06:00
{t("sshTools.startKeyRecording")}
+7
2025-11-05 10:36:16 -06:00
</Button>
2025-11-17 09:46:05 -06:00
) : (
<Button
onClick={handleStopRecording}
className="flex-1"
variant="destructive"
>
{t("sshTools.stopKeyRecording")}
</Button>
)}
</div>
{isRecording && (
<>
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground">
2025-11-17 09:46:05 -06:00
{t("sshTools.selectTerminals")}
</label>
+1
2025-12-31 22:20:12 -06:00
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto thin-scrollbar">
2025-11-17 09:46:05 -06:00
{terminalTabs.map((tab) => (
<Button
key={tab.id}
type="button"
variant="outline"
size="sm"
className={`rounded-full px-3 py-1 text-xs flex items-center gap-1 ${
selectedTabIds.includes(tab.id)
+1
2025-12-31 22:20:12 -06:00
? "text-foreground bg-surface"
: "text-foreground-subtle"
2025-11-17 09:46:05 -06:00
}`}
onClick={() => handleTabToggle(tab.id)}
>
{tab.title}
</Button>
))}
</div>
</div>
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground">
2025-11-17 09:46:05 -06:00
{t("sshTools.typeCommands")}
</label>
<Input
id="ssh-tools-input"
placeholder={t("placeholders.typeHere")}
onKeyDown={handleKeyDown}
onKeyPress={handleKeyPress}
className="font-mono"
disabled={selectedTabIds.length === 0}
readOnly
/>
<p className="text-xs text-muted-foreground">
{t("sshTools.commandsWillBeSent", {
count: selectedTabIds.length,
})}
</p>
</div>
</>
)}
</div>
<Separator />
+1
2025-12-31 22:20:12 -06:00
<h3 className="font-semibold text-foreground">
2025-11-17 09:46:05 -06:00
{t("sshTools.settings")}
</h3>
<div className="flex items-center space-x-2">
<Checkbox
id="enable-copy-paste"
onCheckedChange={updateRightClickCopyPaste}
checked={rightClickCopyPaste}
/>
<label
htmlFor="enable-copy-paste"
+1
2025-12-31 22:20:12 -06:00
className="text-sm font-medium leading-none text-foreground cursor-pointer"
2025-11-17 09:46:05 -06:00
>
{t("sshTools.enableRightClickCopyPaste")}
</label>
</div>
</TabsContent>
<TabsContent
value="snippets"
className="space-y-4 flex flex-col flex-1 overflow-hidden"
>
<div className="flex-shrink-0 space-y-4">
{terminalTabs.length > 0 && (
<>
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground">
{t("snippets.selectTerminals")}
2025-11-17 09:46:05 -06:00
</label>
<p className="text-xs text-muted-foreground">
{selectedSnippetTabIds.length > 0
? t("snippets.executeOnSelected", {
count: selectedSnippetTabIds.length,
})
+1
2025-12-31 22:20:12 -06:00
: t("snippets.executeOnCurrent")}
2025-11-17 09:46:05 -06:00
</p>
+1
2025-12-31 22:20:12 -06:00
<div className="flex flex-wrap gap-2 max-h-32 overflow-y-auto thin-scrollbar">
2025-11-17 09:46:05 -06:00
{terminalTabs.map((tab) => (
<Button
key={tab.id}
type="button"
variant="outline"
size="sm"
className={`rounded-full px-3 py-1 text-xs flex items-center gap-1 ${
selectedSnippetTabIds.includes(tab.id)
+1
2025-12-31 22:20:12 -06:00
? "text-foreground bg-surface"
: "text-foreground-subtle"
2025-11-17 09:46:05 -06:00
}`}
onClick={() => handleSnippetTabToggle(tab.id)}
>
{tab.title}
</Button>
))}
</div>
</div>
<Separator />
</>
)}
+1
2025-12-31 22:20:12 -06:00
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t("snippets.searchSnippets")}
value={snippetSearchQuery}
onChange={(e) => {
setSnippetSearchQuery(e.target.value);
}}
className="pl-10 pr-10"
/>
{snippetSearchQuery && (
<Button
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0"
onClick={() => setSnippetSearchQuery("")}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
2025-11-17 09:46:05 -06:00
<div className="flex gap-2">
<Button
onClick={handleCreate}
className="flex-1"
variant="outline"
>
<Plus className="w-4 h-4 mr-2" />
{t("snippets.new")}
</Button>
<Button
onClick={handleCreateFolder}
className="flex-1"
variant="outline"
>
<FolderPlus className="w-4 h-4 mr-2" />
+1
2025-12-31 22:20:12 -06:00
{t("snippets.newFolder")}
2025-11-17 09:46:05 -06:00
</Button>
+7
2025-11-05 10:36:16 -06:00
</div>
</div>
2025-11-17 09:46:05 -06:00
{loading ? (
<div className="text-center text-muted-foreground py-8 flex-1">
<p>{t("common.loading")}</p>
</div>
) : snippets.length === 0 && snippetFolders.length === 0 ? (
<div className="text-center text-muted-foreground py-8 flex-1">
<p className="mb-2 font-medium">
{t("snippets.empty")}
</p>
<p className="text-sm">{t("snippets.emptyHint")}</p>
</div>
) : (
<TooltipProvider>
+1
2025-12-31 22:20:12 -06:00
<div className="space-y-3 overflow-y-auto flex-1 min-h-0 thin-scrollbar">
2025-11-17 09:46:05 -06:00
{Array.from(groupSnippetsByFolder()).map(
([folderName, folderSnippets]) => {
const folderMetadata = snippetFolders.find(
(f) => f.name === folderName,
);
const isCollapsed =
collapsedFolders.has(folderName);
return (
<div key={folderName || "uncategorized"}>
+1
2025-12-31 22:20:12 -06:00
<div className="flex items-center gap-2 mb-2 hover:bg-hover-alt p-2 rounded-lg transition-colors group/folder">
2025-11-17 09:46:05 -06:00
<div
className="flex items-center gap-2 flex-1 cursor-pointer"
onClick={() => toggleFolder(folderName)}
>
{isCollapsed ? (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
)}
{(() => {
const FolderIcon =
getFolderIcon(folderName);
const folderColor =
getFolderColor(folderName);
return (
<FolderIcon
className="h-4 w-4"
style={{
color: folderColor || undefined,
}}
/>
);
})()}
<span
className="text-sm font-semibold"
style={{
color:
getFolderColor(folderName) ||
undefined,
}}
>
{folderName ||
t("snippets.uncategorized", {
defaultValue: "Uncategorized",
})}
</span>
<span className="text-xs text-muted-foreground ml-auto">
{folderSnippets.length}
</span>
</div>
{folderName && (
<div className="flex items-center gap-1 opacity-0 group-hover/folder:opacity-100 transition-opacity">
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0"
onClick={(e) => {
e.stopPropagation();
handleEditFolder(
folderMetadata || {
id: 0,
userId: "",
name: folderName,
createdAt: "",
updatedAt: "",
},
);
}}
>
<Settings className="h-3 w-3" />
</Button>
<Button
size="sm"
variant="ghost"
className="h-6 w-6 p-0 hover:bg-destructive hover:text-destructive-foreground"
onClick={(e) => {
e.stopPropagation();
handleDeleteFolder(folderName);
}}
>
<Trash2 className="h-3 w-3" />
</Button>
</div>
)}
</div>
{!isCollapsed && (
<div className="space-y-2 ml-6">
{folderSnippets.map((snippet) => (
<div
key={snippet.id}
draggable
onDragStart={(e) =>
handleDragStart(e, snippet)
}
onDragOver={(e) =>
handleDragOver(e, snippet)
}
onDrop={(e) => handleDrop(e, snippet)}
onDragEnd={handleDragEnd}
+1
2025-12-31 22:20:12 -06:00
className={`bg-field border border-input rounded-lg cursor-move hover:shadow-lg hover:border-edge-hover hover:bg-hover-alt transition-all duration-200 p-3 group ${
2025-11-17 09:46:05 -06:00
draggedSnippet?.id === snippet.id
? "opacity-50"
: ""
}`}
>
<div className="mb-2 flex items-center gap-2">
<GripVertical className="h-4 w-4 text-muted-foreground flex-shrink-0 opacity-50 group-hover:opacity-100 transition-opacity" />
<div className="flex-1 min-w-0">
+1
2025-12-31 22:20:12 -06:00
<h3 className="text-sm font-medium text-foreground mb-1">
2025-11-17 09:46:05 -06:00
{snippet.name}
</h3>
{snippet.description && (
<p className="text-xs text-muted-foreground">
{snippet.description}
</p>
)}
<p className="text-xs text-muted-foreground mt-1">
ID: {snippet.id}
</p>
</div>
</div>
<div className="bg-muted/30 rounded p-2 mb-3">
<code className="text-xs font-mono break-all line-clamp-2 text-muted-foreground">
{snippet.content}
</code>
</div>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="default"
className="flex-1"
onClick={() =>
handleExecute(snippet)
}
>
<Play className="w-3 h-3 mr-1" />
{t("snippets.run")}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{t("snippets.runTooltip")}
</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={() =>
handleCopy(snippet)
}
>
<Copy className="w-3 h-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{t("snippets.copyTooltip")}
</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={() =>
handleEdit(snippet)
}
>
<Edit className="w-3 h-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{t("snippets.editTooltip")}
</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="outline"
onClick={() =>
handleDelete(snippet)
}
className="hover:bg-destructive hover:text-destructive-foreground"
>
<Trash2 className="w-3 h-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{t("snippets.deleteTooltip")}
</p>
</TooltipContent>
</Tooltip>
</div>
</div>
))}
</div>
)}
</div>
);
},
)}
</div>
</TooltipProvider>
)}
</TabsContent>
<TabsContent
value="command-history"
className="flex flex-col flex-1 overflow-hidden"
>
<div className="space-y-2 flex-shrink-0 mb-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
+1
2025-12-31 22:20:12 -06:00
placeholder={t("commandHistory.searchPlaceholder")}
2025-11-17 09:46:05 -06:00
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
}}
className="pl-10 pr-10"
/>
{searchQuery && (
<Button
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-7 w-7 p-0"
onClick={() => setSearchQuery("")}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
<p className="text-xs text-muted-foreground bg-muted/30 px-2 py-1.5 rounded">
+1
2025-12-31 22:20:12 -06:00
{t("commandHistory.tabHint")}
+7
2025-11-05 10:36:16 -06:00
</p>
</div>
2025-11-17 09:46:05 -06:00
<div className="flex-1 overflow-hidden min-h-0">
{historyError ? (
<div className="text-center py-8">
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4 mb-4">
<p className="text-destructive font-medium mb-2">
+1
2025-12-31 22:20:12 -06:00
{t("commandHistory.error")}
2025-11-17 09:46:05 -06:00
</p>
<p className="text-sm text-muted-foreground">
{historyError}
</p>
</div>
<Button
onClick={() =>
setHistoryRefreshCounter((prev) => prev + 1)
}
variant="outline"
>
+1
2025-12-31 22:20:12 -06:00
{t("common.retry")}
2025-11-17 09:46:05 -06:00
</Button>
</div>
) : !activeTerminal ? (
<div className="text-center text-muted-foreground py-8">
<Terminal className="h-12 w-12 mb-4 opacity-20 mx-auto" />
<p className="mb-2 font-medium">
+1
2025-12-31 22:20:12 -06:00
{t("commandHistory.noTerminal")}{" "}
2025-11-17 09:46:05 -06:00
</p>
<p className="text-sm">
+1
2025-12-31 22:20:12 -06:00
{t("commandHistory.noTerminalHint")}
2025-11-17 09:46:05 -06:00
</p>
</div>
) : isHistoryLoading && commandHistory.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
<Loader2 className="h-12 w-12 mb-4 opacity-20 mx-auto animate-spin" />
<p className="mb-2 font-medium">
+1
2025-12-31 22:20:12 -06:00
{t("commandHistory.loading")}{" "}
2025-11-17 09:46:05 -06:00
</p>
</div>
) : filteredCommands.length === 0 ? (
<div className="text-center text-muted-foreground py-8">
{searchQuery ? (
<>
<Search className="h-12 w-12 mb-2 opacity-20 mx-auto" />
<p className="mb-2 font-medium">
+1
2025-12-31 22:20:12 -06:00
{t("commandHistory.noResults")}
2025-11-17 09:46:05 -06:00
</p>
<p className="text-sm">
{t("commandHistory.noResultsHint", {
query: searchQuery,
})}
</p>
</>
) : (
<>
<p className="mb-2 font-medium">
+1
2025-12-31 22:20:12 -06:00
{t("commandHistory.empty")}
2025-11-17 09:46:05 -06:00
</p>
<p className="text-sm">
+1
2025-12-31 22:20:12 -06:00
{t("commandHistory.emptyHint")}
2025-11-17 09:46:05 -06:00
</p>
</>
)}
</div>
) : (
<div
ref={commandHistoryScrollRef}
+1
2025-12-31 22:20:12 -06:00
className="space-y-2 overflow-y-auto h-full thin-scrollbar"
2025-11-17 09:46:05 -06:00
>
{filteredCommands.map((command, index) => (
<div
key={index}
+1
2025-12-31 22:20:12 -06:00
className="bg-canvas border-2 border-edge rounded-md px-3 py-2.5 hover:bg-hover-alt hover:border-edge-hover transition-all duration-200 group h-12 flex items-center"
2025-11-17 09:46:05 -06:00
>
<div className="flex items-center justify-between gap-2 w-full min-w-0">
<span
+1
2025-12-31 22:20:12 -06:00
className="flex-1 font-mono text-sm cursor-pointer text-foreground truncate"
2025-11-17 09:46:05 -06:00
onClick={() => handleCommandSelect(command)}
title={command}
>
{command}
</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 hover:bg-red-500/20 hover:text-red-400 flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
handleCommandDelete(command);
}}
+1
2025-12-31 22:20:12 -06:00
title={t("commandHistory.deleteTooltip")}
2025-11-17 09:46:05 -06:00
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
</TabsContent>
<TabsContent
value="split-screen"
className="flex flex-col flex-1 overflow-hidden"
>
+1
2025-12-31 22:20:12 -06:00
<div className="space-y-4 flex-1 overflow-y-auto overflow-x-hidden pb-4 thin-scrollbar">
2025-11-17 09:46:05 -06:00
<Tabs
value={splitMode}
onValueChange={(value) =>
handleSplitModeChange(
+5
2026-03-08 18:02:14 -05:00
value as "none" | "2" | "3" | "4" | "5" | "6",
2025-11-17 09:46:05 -06:00
)
}
className="w-full"
>
+5
2026-03-08 18:02:14 -05:00
<TabsList className="w-full grid grid-cols-3 grid-rows-2 h-auto gap-2 p-2">
<TabsTrigger value="none" className="h-10">
+1
2025-12-31 22:20:12 -06:00
{t("splitScreen.none")}
2025-11-17 09:46:05 -06:00
</TabsTrigger>
+5
2026-03-08 18:02:14 -05:00
<TabsTrigger value="2" className="h-10">
{t("splitScreen.twoSplit")}
2025-11-17 09:46:05 -06:00
</TabsTrigger>
+5
2026-03-08 18:02:14 -05:00
<TabsTrigger value="3" className="h-10">
{t("splitScreen.threeSplit")}
2025-11-17 09:46:05 -06:00
</TabsTrigger>
+5
2026-03-08 18:02:14 -05:00
<TabsTrigger value="4" className="h-10">
{t("splitScreen.fourSplit")}
</TabsTrigger>
<TabsTrigger value="5" className="h-10">
{t("splitScreen.fiveSplit")}
</TabsTrigger>
<TabsTrigger value="6" className="h-10">
{t("splitScreen.sixSplit")}
2025-11-17 09:46:05 -06:00
</TabsTrigger>
</TabsList>
</Tabs>
{splitMode !== "none" && (
<>
<Separator />
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground">
{t("splitScreen.availableTabs")}
2025-11-17 09:46:05 -06:00
</label>
<p className="text-xs text-muted-foreground mb-2">
+1
2025-12-31 22:20:12 -06:00
{t("splitScreen.dragTabsHint")}
2025-11-17 09:46:05 -06:00
</p>
+1
2025-12-31 22:20:12 -06:00
<div className="space-y-1 max-h-[200px] overflow-y-auto thin-scrollbar">
2025-11-17 09:46:05 -06:00
{splittableTabs.map((tab) => {
const isAssigned = Array.from(
splitAssignments.values(),
).includes(tab.id);
const isDragging = draggedTabId === tab.id;
return (
<div
key={tab.id}
draggable={!isAssigned}
onDragStart={() =>
handleTabDragStart(tab.id)
}
onDragEnd={handleTabDragEnd}
className={`
px-3 py-2 rounded-md text-sm cursor-move transition-all
${
isAssigned
+1
2025-12-31 22:20:12 -06:00
? "bg-canvas/50 text-muted-foreground cursor-not-allowed opacity-50"
: "bg-canvas border border-edge hover:border-edge-hover hover:bg-field"
2025-11-17 09:46:05 -06:00
}
${isDragging ? "opacity-50" : ""}
`}
>
<span className="truncate">
{tab.title}
</span>
</div>
);
})}
</div>
</div>
<Separator />
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground">
{t("splitScreen.layout")}
2025-11-17 09:46:05 -06:00
</label>
<div
+1
2025-12-31 22:20:12 -06:00
className={`grid gap-2 mt-2 ${
2025-11-17 09:46:05 -06:00
splitMode === "2"
? "grid-cols-2"
+5
2026-03-08 18:02:14 -05:00
: splitMode === "5" || splitMode === "6"
? "grid-cols-3 grid-rows-2"
2025-11-17 09:46:05 -06:00
: "grid-cols-2 grid-rows-2"
}`}
>
{Array.from(
{ length: parseInt(splitMode) },
(_, idx) => {
const assignedTabId =
splitAssignments.get(idx);
const assignedTab = assignedTabId
+1
2025-12-31 22:20:12 -06:00
? splittableTabs.find(
(t) => t.id === assignedTabId,
)
2025-11-17 09:46:05 -06:00
: null;
const isHovered = dragOverCellIndex === idx;
const isEmpty = !assignedTabId;
return (
<div
key={idx}
onDragOver={(e) =>
handleTabDragOver(e, idx)
}
onDragLeave={handleTabDragLeave}
onDrop={() => handleTabDrop(idx)}
className={`
+1
2025-12-31 22:20:12 -06:00
relative bg-canvas border-2 rounded-md p-3 min-h-[100px]
2025-11-17 09:46:05 -06:00
flex flex-col items-center justify-center transition-all
${splitMode === "3" && idx === 2 ? "col-span-2" : ""}
${
isEmpty
+1
2025-12-31 22:20:12 -06:00
? "border-dashed border-edge"
: "border-solid border-edge-hover bg-surface"
2025-11-17 09:46:05 -06:00
}
${
isHovered && draggedTabId
+1
2025-12-31 22:20:12 -06:00
? "border-edge-hover bg-surface ring-2 ring-edge-hover"
2025-11-17 09:46:05 -06:00
: ""
}
`}
>
{assignedTab ? (
<>
+1
2025-12-31 22:20:12 -06:00
<span className="text-sm text-foreground truncate w-full text-center mb-2">
2025-11-17 09:46:05 -06:00
{assignedTab.title}
</span>
<Button
variant="ghost"
size="sm"
onClick={() =>
handleRemoveFromCell(idx)
}
className="h-6 text-xs hover:bg-red-500/20"
>
+1
2025-12-31 22:20:12 -06:00
{t("common.remove")}
2025-11-17 09:46:05 -06:00
</Button>
</>
) : (
<span className="text-xs text-muted-foreground">
+1
2025-12-31 22:20:12 -06:00
{t("splitScreen.dropHere")}
2025-11-17 09:46:05 -06:00
</span>
)}
</div>
);
},
)}
</div>
</div>
<div className="flex gap-2 pt-2">
<Button
onClick={handleApplySplit}
className="flex-1"
disabled={splitAssignments.size === 0}
>
+1
2025-12-31 22:20:12 -06:00
{t("splitScreen.apply")}
2025-11-17 09:46:05 -06:00
</Button>
<Button
variant="outline"
onClick={handleClearSplit}
className="flex-1"
>
+1
2025-12-31 22:20:12 -06:00
{t("splitScreen.clear")}
2025-11-17 09:46:05 -06:00
</Button>
</div>
</>
)}
{splitMode === "none" && (
<div className="text-center py-8">
<LayoutGrid className="h-12 w-12 mb-4 opacity-20 mx-auto" />
<p className="text-sm text-muted-foreground mb-2">
+1
2025-12-31 22:20:12 -06:00
{t("splitScreen.selectMode")}
2025-11-17 09:46:05 -06:00
</p>
<p className="text-xs text-muted-foreground">
+1
2025-12-31 22:20:12 -06:00
{t("splitScreen.helpText")}
2025-11-17 09:46:05 -06:00
</p>
</div>
)}
</div>
</TabsContent>
</Tabs>
</SidebarContent>
{isOpen && (
<div
className="absolute top-0 h-full cursor-col-resize z-[60]"
onMouseDown={handleMouseDown}
style={{
+1
2025-12-31 22:20:12 -06:00
left: "-4px",
width: "8px",
2025-11-17 09:46:05 -06:00
backgroundColor: isResizing
+1
2025-12-31 22:20:12 -06:00
? "var(--bg-active)"
2025-11-17 09:46:05 -06:00
: "transparent",
}}
onMouseEnter={(e) => {
if (!isResizing) {
e.currentTarget.style.backgroundColor =
+1
2025-12-31 22:20:12 -06:00
"var(--border-hover)";
2025-11-17 09:46:05 -06:00
}
}}
onMouseLeave={(e) => {
if (!isResizing) {
e.currentTarget.style.backgroundColor = "transparent";
}
}}
+1
2025-12-31 22:20:12 -06:00
title={t("common.dragToResizeSidebar")}
2025-11-17 09:46:05 -06:00
/>
)}
</Sidebar>
</SidebarProvider>
</div>
)}
{showDialog && (
<div
className="fixed inset-0 flex items-center justify-center z-[9999999] bg-black/50 backdrop-blur-sm"
onClick={() => setShowDialog(false)}
>
<div
+1
2025-12-31 22:20:12 -06:00
className="bg-canvas border-2 border-edge rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto thin-scrollbar"
2025-11-17 09:46:05 -06:00
onClick={(e) => e.stopPropagation()}
>
<div className="mb-6">
+1
2025-12-31 22:20:12 -06:00
<h2 className="text-xl font-semibold text-foreground">
2025-11-17 09:46:05 -06:00
{editingSnippet ? t("snippets.edit") : t("snippets.create")}
</h2>
<p className="text-sm text-muted-foreground mt-1">
{editingSnippet
? t("snippets.editDescription")
: t("snippets.createDescription")}
</p>
</div>
<div className="space-y-5">
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground flex items-center gap-1">
2025-11-17 09:46:05 -06:00
{t("snippets.name")}
<span className="text-destructive">*</span>
</label>
<Input
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
placeholder={t("snippets.namePlaceholder")}
className={`${formErrors.name ? "border-destructive focus-visible:ring-destructive" : ""}`}
autoFocus
/>
{formErrors.name && (
<p className="text-xs text-destructive mt-1">
{t("snippets.nameRequired")}
</p>
)}
</div>
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground">
2025-11-17 09:46:05 -06:00
{t("snippets.description")}
<span className="text-muted-foreground ml-1">
({t("common.optional")})
</span>
</label>
<Input
value={formData.description}
onChange={(e) =>
setFormData({ ...formData, description: e.target.value })
}
placeholder={t("snippets.descriptionPlaceholder")}
/>
</div>
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground flex items-center gap-2">
2025-11-17 09:46:05 -06:00
<Folder className="h-4 w-4" />
+1
2025-12-31 22:20:12 -06:00
{t("snippets.folder")}
2025-11-17 09:46:05 -06:00
<span className="text-muted-foreground">
({t("common.optional")})
</span>
</label>
<Select
value={formData.folder || "__no_folder__"}
onValueChange={(value) =>
setFormData({
...formData,
folder: value === "__no_folder__" ? undefined : value,
})
}
>
<SelectTrigger>
+1
2025-12-31 22:20:12 -06:00
<SelectValue placeholder={t("snippets.selectFolder")} />
2025-11-17 09:46:05 -06:00
</SelectTrigger>
<SelectContent>
<SelectItem value="__no_folder__">
+1
2025-12-31 22:20:12 -06:00
{t("snippets.noFolder")}
2025-11-17 09:46:05 -06:00
</SelectItem>
{snippetFolders.map((folder) => {
const FolderIcon = getFolderIcon(folder.name);
return (
<SelectItem key={folder.id} value={folder.name}>
<div className="flex items-center gap-2">
<FolderIcon
className="h-4 w-4"
style={{
color: folder.color || undefined,
}}
/>
<span>{folder.name}</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground flex items-center gap-1">
2025-11-17 09:46:05 -06:00
{t("snippets.content")}
<span className="text-destructive">*</span>
</label>
<Textarea
value={formData.content}
onChange={(e) =>
setFormData({ ...formData, content: e.target.value })
}
placeholder={t("snippets.contentPlaceholder")}
className={`font-mono text-sm ${formErrors.content ? "border-destructive focus-visible:ring-destructive" : ""}`}
rows={10}
/>
{formErrors.content && (
<p className="text-xs text-destructive mt-1">
{t("snippets.contentRequired")}
</p>
+7
2025-11-05 10:36:16 -06:00
)}
</div>
</div>
2025-11-17 09:46:05 -06:00
<Separator className="my-6" />
+7
2025-11-05 10:36:16 -06:00
2025-11-17 09:46:05 -06:00
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => setShowDialog(false)}
className="flex-1"
+7
2025-11-05 10:36:16 -06:00
>
2025-11-17 09:46:05 -06:00
{t("common.cancel")}
</Button>
<Button onClick={handleSubmit} className="flex-1">
{editingSnippet ? t("snippets.edit") : t("snippets.create")}
</Button>
+7
2025-11-05 10:36:16 -06:00
</div>
</div>
</div>
2025-11-17 09:46:05 -06:00
)}
{showFolderDialog && (
<div
className="fixed inset-0 flex items-center justify-center z-[9999999] bg-black/50 backdrop-blur-sm"
onClick={() => setShowFolderDialog(false)}
>
<div
+1
2025-12-31 22:20:12 -06:00
className="bg-canvas border-2 border-edge rounded-lg p-6 max-w-lg w-full mx-4"
2025-11-17 09:46:05 -06:00
onClick={(e) => e.stopPropagation()}
>
<div className="mb-6">
+1
2025-12-31 22:20:12 -06:00
<h2 className="text-xl font-semibold text-foreground">
2025-11-17 09:46:05 -06:00
{editingFolder
+1
2025-12-31 22:20:12 -06:00
? t("snippets.editFolder")
: t("snippets.createFolder")}
2025-11-17 09:46:05 -06:00
</h2>
<p className="text-sm text-muted-foreground mt-1">
{editingFolder
+1
2025-12-31 22:20:12 -06:00
? t("snippets.editFolderDescription")
: t("snippets.createFolderDescription")}
2025-11-17 09:46:05 -06:00
</p>
</div>
<div className="space-y-5">
<div className="space-y-2">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground flex items-center gap-1">
{t("snippets.folderName")}
2025-11-17 09:46:05 -06:00
<span className="text-destructive">*</span>
</label>
<Input
value={folderFormData.name}
onChange={(e) =>
setFolderFormData({
...folderFormData,
name: e.target.value,
})
}
+1
2025-12-31 22:20:12 -06:00
placeholder={t("sshTools.scripts.inputPlaceholder")}
2025-11-17 09:46:05 -06:00
className={`${folderFormErrors.name ? "border-destructive focus-visible:ring-destructive" : ""}`}
autoFocus
/>
{folderFormErrors.name && (
<p className="text-xs text-destructive mt-1">
+1
2025-12-31 22:20:12 -06:00
{t("snippets.folderNameRequired")}
2025-11-17 09:46:05 -06:00
</p>
)}
</div>
<div className="space-y-3">
+1
2025-12-31 22:20:12 -06:00
<Label className="text-base font-semibold text-foreground">
{t("snippets.folderColor")}
2025-11-17 09:46:05 -06:00
</Label>
<div className="grid grid-cols-4 gap-3">
{AVAILABLE_COLORS.map((color) => (
<button
key={color.value}
type="button"
className={`h-12 rounded-md border-2 transition-all hover:scale-105 ${
folderFormData.color === color.value
? "border-white shadow-lg scale-105"
+1
2025-12-31 22:20:12 -06:00
: "border-edge"
2025-11-17 09:46:05 -06:00
}`}
style={{ backgroundColor: color.value }}
onClick={() =>
setFolderFormData({
...folderFormData,
color: color.value,
})
}
title={color.label}
/>
))}
</div>
</div>
<div className="space-y-3">
+1
2025-12-31 22:20:12 -06:00
<Label className="text-base font-semibold text-foreground">
{t("snippets.folderIcon")}
2025-11-17 09:46:05 -06:00
</Label>
<div className="grid grid-cols-5 gap-3">
{AVAILABLE_ICONS.map(({ value, label, Icon }) => (
<button
key={value}
type="button"
className={`h-14 rounded-md border-2 transition-all hover:scale-105 flex items-center justify-center ${
folderFormData.icon === value
? "border-primary bg-primary/10"
+1
2025-12-31 22:20:12 -06:00
: "border-edge bg-elevated"
2025-11-17 09:46:05 -06:00
}`}
onClick={() =>
setFolderFormData({ ...folderFormData, icon: value })
}
title={label}
>
<Icon className="w-6 h-6" />
</button>
))}
</div>
</div>
<div className="space-y-3">
+1
2025-12-31 22:20:12 -06:00
<Label className="text-base font-semibold text-foreground">
{t("snippets.preview")}
2025-11-17 09:46:05 -06:00
</Label>
+1
2025-12-31 22:20:12 -06:00
<div className="flex items-center gap-3 p-4 rounded-md bg-elevated border border-edge">
2025-11-17 09:46:05 -06:00
{(() => {
const IconComponent =
AVAILABLE_ICONS.find(
(i) => i.value === folderFormData.icon,
)?.Icon || Folder;
return (
<IconComponent
className="w-5 h-5"
style={{ color: folderFormData.color }}
/>
);
})()}
<span className="font-medium">
+1
2025-12-31 22:20:12 -06:00
{folderFormData.name || t("snippets.folderName")}
2025-11-17 09:46:05 -06:00
</span>
</div>
</div>
</div>
<Separator className="my-6" />
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => setShowFolderDialog(false)}
className="flex-1"
>
{t("common.cancel")}
</Button>
<Button onClick={handleFolderSubmit} className="flex-1">
{editingFolder
+1
2025-12-31 22:20:12 -06:00
? t("snippets.updateFolder")
: t("snippets.createFolder")}
2025-11-17 09:46:05 -06:00
</Button>
</div>
</div>
</div>
)}
</>
+7
2025-11-05 10:36:16 -06:00
);
}