+
TERMIX
-
diff --git a/src/ui/desktop/admin/AdminSettings.tsx b/src/ui/desktop/admin/AdminSettings.tsx
index 02c45dfd..a25cabc5 100644
--- a/src/ui/desktop/admin/AdminSettings.tsx
+++ b/src/ui/desktop/admin/AdminSettings.tsx
@@ -59,10 +59,14 @@ import {
interface AdminSettingsProps {
isTopbarOpen?: boolean;
+ rightSidebarOpen?: boolean;
+ rightSidebarWidth?: number;
}
export function AdminSettings({
isTopbarOpen = true,
+ rightSidebarOpen = false,
+ rightSidebarWidth = 400,
}: AdminSettingsProps): React.ReactElement {
const { t } = useTranslation();
const { confirmWithToast } = useConfirmation();
@@ -637,7 +641,7 @@ export function AdminSettings({
const bottomMarginPx = 8;
const wrapperStyle: React.CSSProperties = {
marginLeft: leftMarginPx,
- marginRight: 17,
+ marginRight: rightSidebarOpen ? rightSidebarWidth + 17 : 17,
marginTop: topMarginPx,
marginBottom: bottomMarginPx,
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
diff --git a/src/ui/desktop/apps/dashboard/Dashboard.tsx b/src/ui/desktop/apps/dashboard/Dashboard.tsx
index 69e2962e..249dd98c 100644
--- a/src/ui/desktop/apps/dashboard/Dashboard.tsx
+++ b/src/ui/desktop/apps/dashboard/Dashboard.tsx
@@ -50,6 +50,8 @@ interface DashboardProps {
userId: string | null;
}) => void;
isTopbarOpen: boolean;
+ rightSidebarOpen?: boolean;
+ rightSidebarWidth?: number;
}
export function Dashboard({
@@ -58,6 +60,8 @@ export function Dashboard({
onAuthSuccess,
isTopbarOpen,
onSelectView,
+ rightSidebarOpen = false,
+ rightSidebarWidth = 400,
}: DashboardProps): React.ReactElement {
const { t } = useTranslation();
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
@@ -97,6 +101,7 @@ export function Dashboard({
const topMarginPx = isTopbarOpen ? 74 : 26;
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
+ const rightMarginPx = rightSidebarOpen ? rightSidebarWidth + 17 : 17;
const bottomMarginPx = 8;
useEffect(() => {
@@ -336,7 +341,7 @@ export function Dashboard({
className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden flex"
style={{
marginLeft: leftMarginPx,
- marginRight: 17,
+ marginRight: rightMarginPx,
marginTop: topMarginPx,
marginBottom: bottomMarginPx,
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
diff --git a/src/ui/desktop/apps/host-manager/HostManager.tsx b/src/ui/desktop/apps/host-manager/HostManager.tsx
index dff7b923..31b4af93 100644
--- a/src/ui/desktop/apps/host-manager/HostManager.tsx
+++ b/src/ui/desktop/apps/host-manager/HostManager.tsx
@@ -18,6 +18,8 @@ export function HostManager({
isTopbarOpen,
initialTab = "host_viewer",
hostConfig,
+ rightSidebarOpen = false,
+ rightSidebarWidth = 400,
}: HostManagerProps): React.ReactElement {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState(initialTab);
@@ -90,7 +92,7 @@ export function HostManager({
className="bg-dark-bg text-white p-4 pt-0 rounded-lg border-2 border-dark-border flex flex-col min-h-0 overflow-hidden"
style={{
marginLeft: leftMarginPx,
- marginRight: 17,
+ marginRight: rightSidebarOpen ? rightSidebarWidth + 17 : 17,
marginTop: topMarginPx,
marginBottom: bottomMarginPx,
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
diff --git a/src/ui/desktop/apps/terminal/SnippetsSidebar.tsx b/src/ui/desktop/apps/terminal/SnippetsSidebar.tsx
deleted file mode 100644
index 424ec142..00000000
--- a/src/ui/desktop/apps/terminal/SnippetsSidebar.tsx
+++ /dev/null
@@ -1,480 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Textarea } from "@/components/ui/textarea";
-import { Separator } from "@/components/ui/separator";
-import {
- Tooltip,
- TooltipContent,
- TooltipProvider,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
-import { Plus, Play, Edit, Trash2, Copy, X } from "lucide-react";
-import { toast } from "sonner";
-import { useTranslation } from "react-i18next";
-import { useConfirmation } from "@/hooks/use-confirmation.ts";
-import {
- getSnippets,
- createSnippet,
- updateSnippet,
- deleteSnippet,
-} from "@/ui/main-axios";
-import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
-import type { Snippet, SnippetData } from "../../../../types/index.js";
-
-interface TabData {
- id: number;
- type: string;
- title: string;
- terminalRef?: {
- current?: {
- sendInput?: (data: string) => void;
- };
- };
- [key: string]: unknown;
-}
-
-interface SnippetsSidebarProps {
- isOpen: boolean;
- onClose: () => void;
- onExecute: (content: string) => void;
-}
-
-export function SnippetsSidebar({
- isOpen,
- onClose,
- onExecute,
-}: SnippetsSidebarProps) {
- const { t } = useTranslation();
- const { confirmWithToast } = useConfirmation();
- const { tabs } = useTabs() as { tabs: TabData[] };
- const [snippets, setSnippets] = 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 [selectedTabIds, setSelectedTabIds] = useState([]);
-
- useEffect(() => {
- if (isOpen) {
- fetchSnippets();
- }
- }, [isOpen]);
-
- const fetchSnippets = async () => {
- try {
- setLoading(true);
- const data = await getSnippets();
- setSnippets(Array.isArray(data) ? data : []);
- } catch {
- toast.error(t("snippets.failedToFetch"));
- setSnippets([]);
- } 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 || "",
- });
- 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 handleTabToggle = (tabId: number) => {
- setSelectedTabIds((prev) =>
- prev.includes(tabId)
- ? prev.filter((id) => id !== tabId)
- : [...prev, tabId],
- );
- };
-
- const handleExecute = (snippet: Snippet) => {
- if (selectedTabIds.length > 0) {
- selectedTabIds.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: selectedTabIds.length,
- }),
- );
- } else {
- onExecute(snippet.content);
- toast.success(t("snippets.executeSuccess", { name: snippet.name }));
- }
- };
-
- const handleCopy = (snippet: Snippet) => {
- navigator.clipboard.writeText(snippet.content);
- toast.success(t("snippets.copySuccess", { name: snippet.name }));
- };
-
- const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal");
-
- if (!isOpen) return null;
-
- return (
- <>
-
-
-
-
e.stopPropagation()}
- >
-
-
- {t("snippets.title")}
-
-
-
-
-
-
- {terminalTabs.length > 0 && (
- <>
-
-
-
- {selectedTabIds.length > 0
- ? t("snippets.executeOnSelected", {
- defaultValue: `Execute on ${selectedTabIds.length} selected terminal(s)`,
- count: selectedTabIds.length,
- })
- : t("snippets.executeOnCurrent", {
- defaultValue:
- "Execute on current terminal (click to select multiple)",
- })}
-
-
- {terminalTabs.map((tab) => (
-
- ))}
-
-
-
- >
- )}
-
-
-
- {loading ? (
-
-
{t("common.loading")}
-
- ) : snippets.length === 0 ? (
-
-
{t("snippets.empty")}
-
{t("snippets.emptyHint")}
-
- ) : (
-
-
- {snippets.map((snippet) => (
-
-
-
- {snippet.name}
-
- {snippet.description && (
-
- {snippet.description}
-
- )}
-
-
-
-
- {snippet.content}
-
-
-
-
-
-
-
-
-
- {t("snippets.runTooltip")}
-
-
-
-
-
-
-
-
- {t("snippets.copyTooltip")}
-
-
-
-
-
-
-
-
- {t("snippets.editTooltip")}
-
-
-
-
-
-
-
-
- {t("snippets.deleteTooltip")}
-
-
-
-
- ))}
-
-
- )}
-
-
-
-
-
- {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")}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
- >
- );
-}
diff --git a/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx b/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx
deleted file mode 100644
index 2317434f..00000000
--- a/src/ui/desktop/apps/tools/SSHToolsSidebar.tsx
+++ /dev/null
@@ -1,351 +0,0 @@
-import React, { useState } from "react";
-import { Button } from "@/components/ui/button.tsx";
-import { Input } from "@/components/ui/input.tsx";
-import { Checkbox } from "@/components/ui/checkbox.tsx";
-import { Separator } from "@/components/ui/separator.tsx";
-import { useTranslation } from "react-i18next";
-import { getCookie, setCookie } from "@/ui/main-axios.ts";
-import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
-import { X } from "lucide-react";
-
-interface TabData {
- id: number;
- type: string;
- title: string;
- terminalRef?: {
- current?: {
- sendInput?: (data: string) => void;
- };
- };
- [key: string]: unknown;
-}
-
-interface SshToolsSidebarProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-export function SSHToolsSidebar({
- isOpen,
- onClose,
-}: SshToolsSidebarProps): React.ReactElement | null {
- const { t } = useTranslation();
- const { tabs } = useTabs() as { tabs: TabData[] };
-
- const [isRecording, setIsRecording] = useState(false);
- const [selectedTabIds, setSelectedTabIds] = useState([]);
-
- 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"; // Ctrl+C (SIGINT)
- e.preventDefault();
- } else if (e.key === "d") {
- commandToSend = "\x04"; // Ctrl+D (EOF)
- e.preventDefault();
- } else if (e.key === "l") {
- commandToSend = "\x0c"; // Ctrl+L (clear screen)
- e.preventDefault();
- } else if (e.key === "u") {
- commandToSend = "\x15"; // Ctrl+U (clear line)
- e.preventDefault();
- } else if (e.key === "k") {
- commandToSend = "\x0b"; // Ctrl+K (clear from cursor to end)
- e.preventDefault();
- } else if (e.key === "a") {
- commandToSend = "\x01"; // Ctrl+A (move to beginning of line)
- e.preventDefault();
- } else if (e.key === "e") {
- commandToSend = "\x05"; // Ctrl+E (move to end of line)
- e.preventDefault();
- } else if (e.key === "w") {
- commandToSend = "\x17"; // Ctrl+W (delete word before cursor)
- e.preventDefault();
- }
- } else if (e.key === "Enter") {
- commandToSend = "\n";
- e.preventDefault();
- } else if (e.key === "Backspace") {
- commandToSend = "\x08"; // Backspace
- e.preventDefault();
- } else if (e.key === "Delete") {
- commandToSend = "\x7f"; // Delete
- e.preventDefault();
- } else if (e.key === "Tab") {
- commandToSend = "\x09"; // Tab
- e.preventDefault();
- } else if (e.key === "Escape") {
- commandToSend = "\x1b"; // Escape
- e.preventDefault();
- } else if (e.key === "ArrowUp") {
- commandToSend = "\x1b[A"; // Up arrow
- e.preventDefault();
- } else if (e.key === "ArrowDown") {
- commandToSend = "\x1b[B"; // Down arrow
- e.preventDefault();
- } else if (e.key === "ArrowLeft") {
- commandToSend = "\x1b[D"; // Left arrow
- e.preventDefault();
- } else if (e.key === "ArrowRight") {
- commandToSend = "\x1b[C"; // Right arrow
- e.preventDefault();
- } else if (e.key === "Home") {
- commandToSend = "\x1b[H"; // Home
- e.preventDefault();
- } else if (e.key === "End") {
- commandToSend = "\x1b[F"; // End
- e.preventDefault();
- } else if (e.key === "PageUp") {
- commandToSend = "\x1b[5~"; // Page Up
- e.preventDefault();
- } else if (e.key === "PageDown") {
- commandToSend = "\x1b[6~"; // Page Down
- e.preventDefault();
- } else if (e.key === "Insert") {
- commandToSend = "\x1b[2~"; // Insert
- e.preventDefault();
- } else if (e.key === "F1") {
- commandToSend = "\x1bOP"; // F1
- e.preventDefault();
- } else if (e.key === "F2") {
- commandToSend = "\x1bOQ"; // F2
- e.preventDefault();
- } else if (e.key === "F3") {
- commandToSend = "\x1bOR"; // F3
- e.preventDefault();
- } else if (e.key === "F4") {
- commandToSend = "\x1bOS"; // F4
- e.preventDefault();
- } else if (e.key === "F5") {
- commandToSend = "\x1b[15~"; // F5
- e.preventDefault();
- } else if (e.key === "F6") {
- commandToSend = "\x1b[17~"; // F6
- e.preventDefault();
- } else if (e.key === "F7") {
- commandToSend = "\x1b[18~"; // F7
- e.preventDefault();
- } else if (e.key === "F8") {
- commandToSend = "\x1b[19~"; // F8
- e.preventDefault();
- } else if (e.key === "F9") {
- commandToSend = "\x1b[20~"; // F9
- e.preventDefault();
- } else if (e.key === "F10") {
- commandToSend = "\x1b[21~"; // F10
- e.preventDefault();
- } else if (e.key === "F11") {
- commandToSend = "\x1b[23~"; // F11
- e.preventDefault();
- } else if (e.key === "F12") {
- commandToSend = "\x1b[24~"; // F12
- 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());
- };
-
- const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal");
-
- if (!isOpen) return null;
-
- return (
-
-
-
-
e.stopPropagation()}
- >
-
-
- {t("sshTools.title")}
-
-
-
-
-
-
-
{t("sshTools.keyRecording")}
-
-
-
-
- {!isRecording ? (
-
- ) : (
-
- )}
-
-
- {isRecording && (
- <>
-
-
-
- {terminalTabs.map((tab) => (
-
- ))}
-
-
-
-
-
-
-
- {t("sshTools.commandsWillBeSent", {
- count: selectedTabIds.length,
- })}
-
-
- >
- )}
-
-
-
-
-
-
{t("sshTools.settings")}
-
-
-
-
-
-
-
-
-
- {t("sshTools.shareIdeas")}{" "}
-
- GitHub
-
- !
-
-
-
-
-
- );
-}
diff --git a/src/ui/desktop/apps/tools/SSHUtilitySidebar.tsx b/src/ui/desktop/apps/tools/SSHUtilitySidebar.tsx
new file mode 100644
index 00000000..1665976f
--- /dev/null
+++ b/src/ui/desktop/apps/tools/SSHUtilitySidebar.tsx
@@ -0,0 +1,901 @@
+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 {
+ 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,
+ ChevronRight,
+} 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,
+} from "@/ui/main-axios.ts";
+import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
+import type { Snippet, SnippetData } from "../../../../types";
+
+interface TabData {
+ id: number;
+ type: string;
+ title: string;
+ terminalRef?: {
+ current?: {
+ sendInput?: (data: string) => void;
+ };
+ };
+ [key: string]: unknown;
+}
+
+interface SSHUtilitySidebarProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSnippetExecute: (content: string) => void;
+ sidebarWidth: number;
+ setSidebarWidth: (width: number) => void;
+}
+
+export function SSHUtilitySidebar({
+ isOpen,
+ onClose,
+ onSnippetExecute,
+ sidebarWidth,
+ setSidebarWidth,
+}: SSHUtilitySidebarProps) {
+ const { t } = useTranslation();
+ const { confirmWithToast } = useConfirmation();
+ const { tabs } = useTabs() as { tabs: TabData[] };
+ const [activeTab, setActiveTab] = useState("ssh-tools");
+
+ // SSH Tools state
+ const [isRecording, setIsRecording] = useState(false);
+ const [selectedTabIds, setSelectedTabIds] = useState([]);
+
+ // Snippets state
+ const [snippets, setSnippets] = 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(
+ [],
+ );
+
+ // Resize state
+ const [isResizing, setIsResizing] = useState(false);
+ const startXRef = React.useRef(null);
+ const startWidthRef = React.useRef(sidebarWidth);
+
+ const terminalTabs = tabs.filter((tab: TabData) => tab.type === "terminal");
+
+ useEffect(() => {
+ if (isOpen && activeTab === "snippets") {
+ fetchSnippets();
+ }
+ }, [isOpen, activeTab]);
+
+ // Resize handlers
+ 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; // Reversed because we're on the right
+ const newWidth = Math.round(startWidthRef.current + dx);
+ const minWidth = 300;
+ const maxWidth = Math.round(window.innerWidth * 0.5);
+ if (newWidth >= minWidth && newWidth <= maxWidth) {
+ setSidebarWidth(newWidth);
+ } else if (newWidth < minWidth) {
+ setSidebarWidth(minWidth);
+ } else if (newWidth > maxWidth) {
+ setSidebarWidth(maxWidth);
+ }
+ };
+
+ 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, sidebarWidth, setSidebarWidth]);
+
+ // SSH Tools handlers
+ 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());
+ };
+
+ // Snippets handlers
+ const fetchSnippets = async () => {
+ try {
+ setLoading(true);
+ const data = await getSnippets();
+ setSnippets(Array.isArray(data) ? data : []);
+ } catch {
+ toast.error(t("snippets.failedToFetch"));
+ setSnippets([]);
+ } 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 || "",
+ });
+ 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 }));
+ }
+ };
+
+ const handleCopy = (snippet: Snippet) => {
+ navigator.clipboard.writeText(snippet.content);
+ toast.success(t("snippets.copySuccess", { name: snippet.name }));
+ };
+
+ if (!isOpen) return null;
+
+ return (
+ <>
+
+
+
+
+
+
+ {t("nav.tools")}
+
+
+
+
+
+
+
+
+
+
+
+ {t("sshTools.title")}
+
+
+ {t("snippets.title")}
+
+
+
+
+
+ {t("sshTools.keyRecording")}
+
+
+
+
+ {!isRecording ? (
+
+ ) : (
+
+ )}
+
+
+ {isRecording && (
+ <>
+
+
+
+ {terminalTabs.map((tab) => (
+
+ ))}
+
+
+
+
+
+
+
+ {t("sshTools.commandsWillBeSent", {
+ count: selectedTabIds.length,
+ })}
+
+
+ >
+ )}
+
+
+
+
+
+ {t("sshTools.settings")}
+
+
+
+
+
+
+
+
+
+
+ {t("sshTools.shareIdeas")}{" "}
+
+ GitHub
+
+ !
+
+
+
+
+ {terminalTabs.length > 0 && (
+ <>
+
+
+
+ {selectedSnippetTabIds.length > 0
+ ? t("snippets.executeOnSelected", {
+ defaultValue: `Execute on ${selectedSnippetTabIds.length} selected terminal(s)`,
+ count: selectedSnippetTabIds.length,
+ })
+ : t("snippets.executeOnCurrent", {
+ defaultValue:
+ "Execute on current terminal (click to select multiple)",
+ })}
+
+
+ {terminalTabs.map((tab) => (
+
+ ))}
+
+
+
+ >
+ )}
+
+
+
+ {loading ? (
+
+
{t("common.loading")}
+
+ ) : snippets.length === 0 ? (
+
+
+ {t("snippets.empty")}
+
+
{t("snippets.emptyHint")}
+
+ ) : (
+
+
+ {snippets.map((snippet) => (
+
+
+
+ {snippet.name}
+
+ {snippet.description && (
+
+ {snippet.description}
+
+ )}
+
+
+
+
+ {snippet.content}
+
+
+
+
+
+
+
+
+
+ {t("snippets.runTooltip")}
+
+
+
+
+
+
+
+
+ {t("snippets.copyTooltip")}
+
+
+
+
+
+
+
+
+ {t("snippets.editTooltip")}
+
+
+
+
+
+
+
+
+ {t("snippets.deleteTooltip")}
+
+
+
+
+ ))}
+
+
+ )}
+
+
+
+ {isOpen && (
+ {
+ if (!isResizing) {
+ e.currentTarget.style.backgroundColor =
+ "var(--dark-border-hover)";
+ }
+ }}
+ onMouseLeave={(e) => {
+ if (!isResizing) {
+ e.currentTarget.style.backgroundColor = "transparent";
+ }
+ }}
+ title="Drag to resize sidebar"
+ />
+ )}
+
+
+
+
+ {!isOpen && (
+
+
+
+ )}
+
+
+ {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")}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/src/ui/desktop/apps/tools/ToolsMenu.tsx b/src/ui/desktop/apps/tools/ToolsMenu.tsx
deleted file mode 100644
index f4028f1b..00000000
--- a/src/ui/desktop/apps/tools/ToolsMenu.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React from "react";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu.tsx";
-import { Button } from "@/components/ui/button.tsx";
-import { Hammer, Wrench, FileText, Command } from "lucide-react";
-import { useTranslation } from "react-i18next";
-
-interface ToolsMenuProps {
- onOpenSshTools: () => void;
- onOpenSnippets: () => void;
- onOpenCommandPalette: () => void;
-}
-
-export function ToolsMenu({
- onOpenSshTools,
- onOpenSnippets,
- onOpenCommandPalette,
-}: ToolsMenuProps): React.ReactElement {
- const { t } = useTranslation();
-
- return (
-
-
-
-
-
-
-
- {t("sshTools.title")}
-
-
-
- {t("snippets.title")}
-
-
-
-
- Command Palette
-
- LShift LShift
-
-
-
-
-
- );
-}
diff --git a/src/ui/desktop/navigation/AppView.tsx b/src/ui/desktop/navigation/AppView.tsx
index 6efed744..d51c5eba 100644
--- a/src/ui/desktop/navigation/AppView.tsx
+++ b/src/ui/desktop/navigation/AppView.tsx
@@ -35,10 +35,14 @@ interface TabData {
interface TerminalViewProps {
isTopbarOpen?: boolean;
+ rightSidebarOpen?: boolean;
+ rightSidebarWidth?: number;
}
export function AppView({
isTopbarOpen = true,
+ rightSidebarOpen = false,
+ rightSidebarWidth = 400,
}: TerminalViewProps): React.ReactElement {
const { tabs, currentTab, allSplitScreenTab, removeTab } = useTabs() as {
tabs: TabData[];
@@ -648,7 +652,7 @@ export function AppView({
style={{
background: containerBackground,
marginLeft: leftMarginPx,
- marginRight: 17,
+ marginRight: rightSidebarOpen ? rightSidebarWidth + 17 : 17,
marginTop: topMarginPx,
marginBottom: bottomMarginPx,
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
diff --git a/src/ui/desktop/navigation/TopNavbar.tsx b/src/ui/desktop/navigation/TopNavbar.tsx
index 2ad540f8..d729464e 100644
--- a/src/ui/desktop/navigation/TopNavbar.tsx
+++ b/src/ui/desktop/navigation/TopNavbar.tsx
@@ -2,14 +2,12 @@ import React, { useState } from "react";
import { flushSync } from "react-dom";
import { useSidebar } from "@/components/ui/sidebar.tsx";
import { Button } from "@/components/ui/button.tsx";
-import { ChevronDown, ChevronUpIcon } from "lucide-react";
+import { ChevronDown, ChevronUpIcon, Hammer } from "lucide-react";
import { Tab } from "@/ui/desktop/navigation/tabs/Tab.tsx";
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
import { useTranslation } from "react-i18next";
import { TabDropdown } from "@/ui/desktop/navigation/tabs/TabDropdown.tsx";
-import { SnippetsSidebar } from "@/ui/desktop/apps/terminal/SnippetsSidebar.tsx";
-import { SSHToolsSidebar } from "@/ui/desktop/apps/tools/SSHToolsSidebar.tsx";
-import { ToolsMenu } from "@/ui/desktop/apps/tools/ToolsMenu.tsx";
+import { SSHUtilitySidebar } from "@/ui/desktop/apps/tools/SSHUtilitySidebar.tsx";
interface TabData {
id: number;
@@ -27,12 +25,14 @@ interface TopNavbarProps {
isTopbarOpen: boolean;
setIsTopbarOpen: (open: boolean) => void;
onOpenCommandPalette: () => void;
+ onRightSidebarStateChange?: (isOpen: boolean, width: number) => void;
}
export function TopNavbar({
isTopbarOpen,
setIsTopbarOpen,
onOpenCommandPalette,
+ onRightSidebarStateChange,
}: TopNavbarProps): React.ReactElement {
const { state } = useSidebar();
const {
@@ -56,8 +56,25 @@ export function TopNavbar({
state === "collapsed" ? "26px" : "calc(var(--sidebar-width) + 8px)";
const { t } = useTranslation();
- const [toolsSheetOpen, setToolsSheetOpen] = useState(false);
- const [snippetsSidebarOpen, setSnippetsSidebarOpen] = useState(false);
+ const [toolsSidebarOpen, setToolsSidebarOpen] = useState(false);
+ const [rightSidebarWidth, setRightSidebarWidth] = useState(() => {
+ const saved = localStorage.getItem("rightSidebarWidth");
+ return saved !== null ? parseInt(saved, 10) : 400;
+ });
+
+ React.useEffect(() => {
+ localStorage.setItem("rightSidebarWidth", String(rightSidebarWidth));
+ }, [rightSidebarWidth]);
+
+ React.useEffect(() => {
+ if (onRightSidebarStateChange) {
+ onRightSidebarStateChange(toolsSidebarOpen, rightSidebarWidth);
+ }
+ }, [toolsSidebarOpen, rightSidebarWidth, onRightSidebarStateChange]);
+
+ const rightPosition = toolsSidebarOpen
+ ? `${rightSidebarWidth + 17}px`
+ : "17px";
const [justDroppedTabId, setJustDroppedTabId] = useState(null);
const [isInDropAnimation, setIsInDropAnimation] = useState(false);
const [dragState, setDragState] = useState<{
@@ -302,7 +319,7 @@ export function TopNavbar({
style={{
top: isTopbarOpen ? "0.5rem" : "-3rem",
left: leftPosition,
- right: "17px",
+ right: rightPosition,
backgroundColor: "#18181b",
}}
>
@@ -475,11 +492,14 @@ export function TopNavbar({
- setToolsSheetOpen(true)}
- onOpenSnippets={() => setSnippetsSidebarOpen(true)}
- onOpenCommandPalette={onOpenCommandPalette}
- />
+
);
diff --git a/src/ui/desktop/user/UserProfile.tsx b/src/ui/desktop/user/UserProfile.tsx
index dfc9c709..79bdaf74 100644
--- a/src/ui/desktop/user/UserProfile.tsx
+++ b/src/ui/desktop/user/UserProfile.tsx
@@ -26,6 +26,8 @@ import { useSidebar } from "@/components/ui/sidebar.tsx";
interface UserProfileProps {
isTopbarOpen?: boolean;
+ rightSidebarOpen?: boolean;
+ rightSidebarWidth?: number;
}
async function handleLogout() {
@@ -68,7 +70,11 @@ async function handleLogout() {
}
}
-export function UserProfile({ isTopbarOpen = true }: UserProfileProps) {
+export function UserProfile({
+ isTopbarOpen = true,
+ rightSidebarOpen = false,
+ rightSidebarWidth = 400,
+}: UserProfileProps) {
const { t } = useTranslation();
const { state: sidebarState } = useSidebar();
const [userInfo, setUserInfo] = useState<{
@@ -156,7 +162,7 @@ export function UserProfile({ isTopbarOpen = true }: UserProfileProps) {
const bottomMarginPx = 8;
const wrapperStyle: React.CSSProperties = {
marginLeft: leftMarginPx,
- marginRight: 17,
+ marginRight: rightSidebarOpen ? rightSidebarWidth + 17 : 17,
marginTop: topMarginPx,
marginBottom: bottomMarginPx,
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,