chore: File cleanup
This commit is contained in:
@@ -63,7 +63,6 @@ export function CredentialEditor({
|
||||
useState(false);
|
||||
const publicKeyDetectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Clear error when tab changes
|
||||
useEffect(() => {
|
||||
setFormError(null);
|
||||
}, [activeTab]);
|
||||
|
||||
@@ -102,7 +102,7 @@ export function Dashboard({
|
||||
|
||||
const topMarginPx = isTopbarOpen ? 74 : 26;
|
||||
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
|
||||
const rightMarginPx = 17; // Base margin when closed
|
||||
const rightMarginPx = 17;
|
||||
const bottomMarginPx = 8;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -213,7 +213,6 @@ export function Dashboard({
|
||||
statsConfig?: string | { metricsEnabled?: boolean };
|
||||
}) => {
|
||||
try {
|
||||
// Parse statsConfig if it's a string
|
||||
let statsConfig: { metricsEnabled?: boolean } = {
|
||||
metricsEnabled: true,
|
||||
};
|
||||
@@ -225,7 +224,6 @@ export function Dashboard({
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if metrics are disabled
|
||||
if (statsConfig.metricsEnabled === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1087,7 +1087,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
t("fileManager.archiveExtractedSuccessfully", { name: file.name }),
|
||||
);
|
||||
|
||||
// Refresh directory to show extracted files
|
||||
handleRefreshDirectory();
|
||||
} catch (error: unknown) {
|
||||
const err = error as { message?: string };
|
||||
@@ -1132,7 +1131,6 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
|
||||
}),
|
||||
);
|
||||
|
||||
// Refresh directory to show compressed file
|
||||
handleRefreshDirectory();
|
||||
clearSelection();
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -261,7 +261,6 @@ export function FileManagerContextMenu({
|
||||
});
|
||||
}
|
||||
|
||||
// Add extract option for archive files
|
||||
if (isSingleFile && files[0].type === "file" && onExtractArchive) {
|
||||
const fileName = files[0].name.toLowerCase();
|
||||
const isArchive =
|
||||
@@ -288,7 +287,6 @@ export function FileManagerContextMenu({
|
||||
}
|
||||
}
|
||||
|
||||
// Add compress option for selected files/folders
|
||||
if (isFileContext && onCompress) {
|
||||
menuItems.push({
|
||||
icon: <FileArchive className="w-4 h-4" />,
|
||||
|
||||
@@ -38,7 +38,6 @@ export function CompressDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (open && fileNames.length > 0) {
|
||||
// Generate default archive name
|
||||
if (fileNames.length === 1) {
|
||||
const baseName = fileNames[0].replace(/\.[^/.]+$/, "");
|
||||
setArchiveName(baseName);
|
||||
@@ -51,7 +50,6 @@ export function CompressDialog({
|
||||
const handleCompress = () => {
|
||||
if (!archiveName.trim()) return;
|
||||
|
||||
// Append extension if not already present
|
||||
let finalName = archiveName.trim();
|
||||
const extensions: Record<string, string> = {
|
||||
zip: ".zip",
|
||||
|
||||
@@ -30,7 +30,6 @@ interface PermissionsDialogProps {
|
||||
onSave: (file: FileItem, permissions: string) => Promise<void>;
|
||||
}
|
||||
|
||||
// Parse permissions like "rwxr-xr-x" or "755" to individual bits
|
||||
const parsePermissions = (
|
||||
perms: string,
|
||||
): { owner: number; group: number; other: number } => {
|
||||
@@ -38,7 +37,6 @@ const parsePermissions = (
|
||||
return { owner: 0, group: 0, other: 0 };
|
||||
}
|
||||
|
||||
// If numeric format like "755"
|
||||
if (/^\d{3,4}$/.test(perms)) {
|
||||
const numStr = perms.slice(-3);
|
||||
return {
|
||||
@@ -47,8 +45,6 @@ const parsePermissions = (
|
||||
other: parseInt(numStr[2] || "0", 10),
|
||||
};
|
||||
}
|
||||
|
||||
// If symbolic format like "rwxr-xr-x" or "-rwxr-xr-x"
|
||||
const cleanPerms = perms.replace(/^-/, "").substring(0, 9);
|
||||
|
||||
const calcBits = (str: string): number => {
|
||||
@@ -66,7 +62,6 @@ const parsePermissions = (
|
||||
};
|
||||
};
|
||||
|
||||
// Convert individual bits to numeric format
|
||||
const toNumeric = (owner: number, group: number, other: number): string => {
|
||||
return `${owner}${group}${other}`;
|
||||
};
|
||||
@@ -99,7 +94,6 @@ export function PermissionsDialog({
|
||||
(initialPerms.other & 1) !== 0,
|
||||
);
|
||||
|
||||
// Reset when file changes
|
||||
useEffect(() => {
|
||||
if (file) {
|
||||
const perms = parsePermissions(file.permissions || "644");
|
||||
|
||||
@@ -72,7 +72,6 @@ export function HostManager({
|
||||
};
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
// Only clear editing state when leaving the respective tabs, not when entering them
|
||||
if (activeTab === "add_host" && value !== "add_host") {
|
||||
setEditingHost(null);
|
||||
}
|
||||
|
||||
@@ -348,7 +348,6 @@ export function HostManagerEditor({
|
||||
const [activeTab, setActiveTab] = useState("general");
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
// Clear error when tab changes
|
||||
useEffect(() => {
|
||||
setFormError(null);
|
||||
}, [activeTab]);
|
||||
@@ -948,7 +947,6 @@ export function HostManagerEditor({
|
||||
|
||||
window.dispatchEvent(new CustomEvent("ssh-hosts:changed"));
|
||||
|
||||
// Notify the stats server to start/update polling for this specific host
|
||||
if (savedHost?.id) {
|
||||
const { notifyHostCreatedOrUpdated } = await import(
|
||||
"@/ui/main-axios.ts"
|
||||
@@ -963,11 +961,9 @@ export function HostManagerEditor({
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form validation errors
|
||||
const handleFormError = () => {
|
||||
const errors = form.formState.errors;
|
||||
|
||||
// Determine which tab contains the error
|
||||
if (
|
||||
errors.ip ||
|
||||
errors.port ||
|
||||
@@ -1088,7 +1084,6 @@ export function HostManagerEditor({
|
||||
|
||||
let filtered = sshConfigurations;
|
||||
|
||||
// Filter out the current host being edited (by ID, not by name)
|
||||
if (currentHostId) {
|
||||
const currentHostName = hosts.find((h) => h.id === currentHostId)?.name;
|
||||
if (currentHostName) {
|
||||
@@ -1097,7 +1092,6 @@ export function HostManagerEditor({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If creating a new host, filter by the name being entered
|
||||
const currentHostName =
|
||||
form.watch("name") || `${form.watch("username")}@${form.watch("ip")}`;
|
||||
filtered = sshConfigurations.filter(
|
||||
|
||||
@@ -114,7 +114,6 @@ export function Server({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hostConfig?.id !== currentHostConfig?.id) {
|
||||
// Reset state when switching to a different host
|
||||
setServerStatus("offline");
|
||||
setMetrics(null);
|
||||
setMetricsHistory([]);
|
||||
|
||||
@@ -25,9 +25,7 @@ interface LoginStatsWidgetProps {
|
||||
metricsHistory: ServerMetrics[];
|
||||
}
|
||||
|
||||
export function LoginStatsWidget({
|
||||
metrics,
|
||||
}: LoginStatsWidgetProps) {
|
||||
export function LoginStatsWidget({ metrics }: LoginStatsWidgetProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const loginStats = metrics?.login_stats;
|
||||
@@ -52,7 +50,9 @@ export function LoginStatsWidget({
|
||||
<Activity className="h-3 w-3" />
|
||||
<span>{t("serverStats.totalLogins")}</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-green-400">{totalLogins}</div>
|
||||
<div className="text-xl font-bold text-green-400">
|
||||
{totalLogins}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-dark-bg-darker p-2 rounded border border-dark-border/30">
|
||||
<div className="flex items-center gap-1 text-xs text-gray-400 mb-1">
|
||||
@@ -86,7 +86,9 @@ export function LoginStatsWidget({
|
||||
<span className="text-green-400 font-mono truncate">
|
||||
{login.user}
|
||||
</span>
|
||||
<span className="text-gray-500">{t("serverStats.from")}</span>
|
||||
<span className="text-gray-500">
|
||||
{t("serverStats.from")}
|
||||
</span>
|
||||
<span className="text-blue-400 font-mono truncate">
|
||||
{login.ip}
|
||||
</span>
|
||||
@@ -118,7 +120,9 @@ export function LoginStatsWidget({
|
||||
<span className="text-red-400 font-mono truncate">
|
||||
{login.user}
|
||||
</span>
|
||||
<span className="text-gray-500">{t("serverStats.from")}</span>
|
||||
<span className="text-gray-500">
|
||||
{t("serverStats.from")}
|
||||
</span>
|
||||
<span className="text-blue-400 font-mono truncate">
|
||||
{login.ip}
|
||||
</span>
|
||||
|
||||
@@ -131,13 +131,11 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const activityLoggedRef = useRef(false);
|
||||
const keyHandlerAttachedRef = useRef(false);
|
||||
|
||||
// Command history tracking (Stage 1)
|
||||
const { trackInput, getCurrentCommand, updateCurrentCommand } =
|
||||
useCommandTracker({
|
||||
hostId: hostConfig.id,
|
||||
enabled: true,
|
||||
onCommandExecuted: (command) => {
|
||||
// Add to autocomplete history (Stage 3)
|
||||
if (!autocompleteHistory.current.includes(command)) {
|
||||
autocompleteHistory.current = [
|
||||
command,
|
||||
@@ -147,7 +145,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
},
|
||||
});
|
||||
|
||||
// Create refs for callbacks to avoid triggering useEffect re-runs
|
||||
const getCurrentCommandRef = useRef(getCurrentCommand);
|
||||
const updateCurrentCommandRef = useRef(updateCurrentCommand);
|
||||
|
||||
@@ -156,7 +153,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
updateCurrentCommandRef.current = updateCurrentCommand;
|
||||
}, [getCurrentCommand, updateCurrentCommand]);
|
||||
|
||||
// Real-time autocomplete (Stage 3)
|
||||
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
||||
const [autocompleteSuggestions, setAutocompleteSuggestions] = useState<
|
||||
string[]
|
||||
@@ -170,23 +166,19 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const autocompleteHistory = useRef<string[]>([]);
|
||||
const currentAutocompleteCommand = useRef<string>("");
|
||||
|
||||
// Refs for accessing current state in event handlers
|
||||
const showAutocompleteRef = useRef(false);
|
||||
const autocompleteSuggestionsRef = useRef<string[]>([]);
|
||||
const autocompleteSelectedIndexRef = useRef(0);
|
||||
|
||||
// Command history dialog (Stage 2)
|
||||
const [showHistoryDialog, setShowHistoryDialog] = useState(false);
|
||||
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
||||
|
||||
// Create refs for context methods to avoid infinite loops
|
||||
const setIsLoadingRef = useRef(commandHistoryContext.setIsLoading);
|
||||
const setCommandHistoryContextRef = useRef(
|
||||
commandHistoryContext.setCommandHistory,
|
||||
);
|
||||
|
||||
// Keep refs updated with latest context methods
|
||||
useEffect(() => {
|
||||
setIsLoadingRef.current = commandHistoryContext.setIsLoading;
|
||||
setCommandHistoryContextRef.current =
|
||||
@@ -196,7 +188,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
commandHistoryContext.setCommandHistory,
|
||||
]);
|
||||
|
||||
// Load command history when dialog opens
|
||||
useEffect(() => {
|
||||
if (showHistoryDialog && hostConfig.id) {
|
||||
setIsLoadingHistory(true);
|
||||
@@ -219,9 +210,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
}, [showHistoryDialog, hostConfig.id]);
|
||||
|
||||
// Load command history for autocomplete on mount (Stage 3)
|
||||
useEffect(() => {
|
||||
// Check if command autocomplete is enabled
|
||||
const autocompleteEnabled =
|
||||
localStorage.getItem("commandAutocomplete") !== "false";
|
||||
|
||||
@@ -240,7 +229,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
}, [hostConfig.id]);
|
||||
|
||||
// Sync autocomplete state to refs for event handlers
|
||||
useEffect(() => {
|
||||
showAutocompleteRef.current = showAutocomplete;
|
||||
}, [showAutocomplete]);
|
||||
@@ -642,9 +630,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}),
|
||||
);
|
||||
terminal.onData((data) => {
|
||||
// Track command input for history (Stage 1)
|
||||
trackInput(data);
|
||||
// Send input to server
|
||||
ws.send(JSON.stringify({ type: "input", data }));
|
||||
});
|
||||
|
||||
@@ -904,20 +890,16 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
return "";
|
||||
}
|
||||
|
||||
// Handle command selection from history dialog (Stage 2)
|
||||
const handleSelectCommand = useCallback(
|
||||
(command: string) => {
|
||||
if (!terminal || !webSocketRef.current) return;
|
||||
|
||||
// Send the command to the terminal
|
||||
// Simulate typing the command character by character
|
||||
for (const char of command) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({ type: "input", data: char }),
|
||||
);
|
||||
}
|
||||
|
||||
// Return focus to terminal after selecting command
|
||||
setTimeout(() => {
|
||||
terminal.focus();
|
||||
}, 100);
|
||||
@@ -925,12 +907,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
[terminal],
|
||||
);
|
||||
|
||||
// Register handlers with context
|
||||
useEffect(() => {
|
||||
commandHistoryContext.setOnSelectCommand(handleSelectCommand);
|
||||
}, [handleSelectCommand]);
|
||||
|
||||
// Handle autocomplete selection (mouse click)
|
||||
const handleAutocompleteSelect = useCallback(
|
||||
(selectedCommand: string) => {
|
||||
if (!webSocketRef.current) return;
|
||||
@@ -938,22 +918,18 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const currentCmd = currentAutocompleteCommand.current;
|
||||
const completion = selectedCommand.substring(currentCmd.length);
|
||||
|
||||
// Send completion characters to server
|
||||
for (const char of completion) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({ type: "input", data: char }),
|
||||
);
|
||||
}
|
||||
|
||||
// Update current command tracker
|
||||
updateCurrentCommand(selectedCommand);
|
||||
|
||||
// Close autocomplete
|
||||
setShowAutocomplete(false);
|
||||
setAutocompleteSuggestions([]);
|
||||
currentAutocompleteCommand.current = "";
|
||||
|
||||
// Return focus to terminal
|
||||
setTimeout(() => {
|
||||
terminal?.focus();
|
||||
}, 50);
|
||||
@@ -963,26 +939,22 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
[terminal, updateCurrentCommand],
|
||||
);
|
||||
|
||||
// Handle command deletion from history dialog
|
||||
const handleDeleteCommand = useCallback(
|
||||
async (command: string) => {
|
||||
if (!hostConfig.id) return;
|
||||
|
||||
try {
|
||||
// Call API to delete command
|
||||
const { deleteCommandFromHistory } = await import(
|
||||
"@/ui/main-axios.ts"
|
||||
);
|
||||
await deleteCommandFromHistory(hostConfig.id, command);
|
||||
|
||||
// Update local state
|
||||
setCommandHistory((prev) => {
|
||||
const newHistory = prev.filter((cmd) => cmd !== command);
|
||||
setCommandHistoryContextRef.current(newHistory);
|
||||
return newHistory;
|
||||
});
|
||||
|
||||
// Update autocomplete history
|
||||
autocompleteHistory.current = autocompleteHistory.current.filter(
|
||||
(cmd) => cmd !== command,
|
||||
);
|
||||
@@ -995,7 +967,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
[hostConfig.id],
|
||||
);
|
||||
|
||||
// Register delete handler with context
|
||||
useEffect(() => {
|
||||
commandHistoryContext.setOnDeleteCommand(handleDeleteCommand);
|
||||
}, [handleDeleteCommand]);
|
||||
@@ -1104,7 +1075,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
navigator.platform.toUpperCase().indexOf("MAC") >= 0 ||
|
||||
navigator.userAgent.toUpperCase().indexOf("MAC") >= 0;
|
||||
|
||||
// Handle Ctrl+R for command history (Stage 2)
|
||||
if (
|
||||
e.ctrlKey &&
|
||||
e.key === "r" &&
|
||||
@@ -1115,7 +1085,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowHistoryDialog(true);
|
||||
// Also trigger the sidebar to open
|
||||
if (commandHistoryContext.openCommandHistory) {
|
||||
commandHistoryContext.openCommandHistory();
|
||||
}
|
||||
@@ -1210,20 +1179,15 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
};
|
||||
}, [xtermRef, terminal, hostConfig]);
|
||||
|
||||
// Register keyboard handler for autocomplete (Stage 3)
|
||||
// Registered only once when terminal is created
|
||||
useEffect(() => {
|
||||
if (!terminal) return;
|
||||
|
||||
const handleCustomKey = (e: KeyboardEvent): boolean => {
|
||||
// Only handle keydown events, ignore keyup to prevent double triggering
|
||||
if (e.type !== "keydown") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If autocomplete is showing, handle keys specially
|
||||
if (showAutocompleteRef.current) {
|
||||
// Handle Escape to close autocomplete
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -1233,7 +1197,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle Arrow keys for autocomplete navigation
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -1253,7 +1216,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle Enter to confirm autocomplete selection
|
||||
if (
|
||||
e.key === "Enter" &&
|
||||
autocompleteSuggestionsRef.current.length > 0
|
||||
@@ -1268,7 +1230,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const currentCmd = currentAutocompleteCommand.current;
|
||||
const completion = selectedCommand.substring(currentCmd.length);
|
||||
|
||||
// Send completion characters to server
|
||||
if (webSocketRef.current?.readyState === 1) {
|
||||
for (const char of completion) {
|
||||
webSocketRef.current.send(
|
||||
@@ -1277,10 +1238,8 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
}
|
||||
}
|
||||
|
||||
// Update current command tracker
|
||||
updateCurrentCommandRef.current(selectedCommand);
|
||||
|
||||
// Close autocomplete
|
||||
setShowAutocomplete(false);
|
||||
setAutocompleteSuggestions([]);
|
||||
currentAutocompleteCommand.current = "";
|
||||
@@ -1288,7 +1247,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle Tab to cycle through suggestions
|
||||
if (
|
||||
e.key === "Tab" &&
|
||||
!e.ctrlKey &&
|
||||
@@ -1306,14 +1264,12 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
return false;
|
||||
}
|
||||
|
||||
// For any other key while autocomplete is showing, close it and let key through
|
||||
setShowAutocomplete(false);
|
||||
setAutocompleteSuggestions([]);
|
||||
currentAutocompleteCommand.current = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle Tab for autocomplete (when autocomplete is not showing)
|
||||
if (
|
||||
e.key === "Tab" &&
|
||||
!e.ctrlKey &&
|
||||
@@ -1324,12 +1280,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Check if command autocomplete is enabled in settings
|
||||
const autocompleteEnabled =
|
||||
localStorage.getItem("commandAutocomplete") !== "false";
|
||||
|
||||
if (!autocompleteEnabled) {
|
||||
// If disabled, let the terminal handle Tab normally (send to server)
|
||||
if (webSocketRef.current?.readyState === 1) {
|
||||
webSocketRef.current.send(
|
||||
JSON.stringify({ type: "input", data: "\t" }),
|
||||
@@ -1340,7 +1294,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
|
||||
const currentCmd = getCurrentCommandRef.current().trim();
|
||||
if (currentCmd.length > 0 && webSocketRef.current?.readyState === 1) {
|
||||
// Filter commands that start with current input
|
||||
const matches = autocompleteHistory.current
|
||||
.filter(
|
||||
(cmd) =>
|
||||
@@ -1348,10 +1301,9 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
cmd !== currentCmd &&
|
||||
cmd.length > currentCmd.length,
|
||||
)
|
||||
.slice(0, 5); // Show up to 5 matches for better UX
|
||||
.slice(0, 5);
|
||||
|
||||
if (matches.length === 1) {
|
||||
// Only one match - auto-complete directly
|
||||
const completedCommand = matches[0];
|
||||
const completion = completedCommand.substring(currentCmd.length);
|
||||
|
||||
@@ -1363,12 +1315,10 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
|
||||
updateCurrentCommandRef.current(completedCommand);
|
||||
} else if (matches.length > 1) {
|
||||
// Multiple matches - show selection list
|
||||
currentAutocompleteCommand.current = currentCmd;
|
||||
setAutocompleteSuggestions(matches);
|
||||
setAutocompleteSelectedIndex(0);
|
||||
|
||||
// Calculate position (below or above cursor based on available space)
|
||||
const cursorY = terminal.buffer.active.cursorY;
|
||||
const cursorX = terminal.buffer.active.cursorX;
|
||||
const rect = xtermRef.current?.getBoundingClientRect();
|
||||
@@ -1379,8 +1329,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
const cellWidth =
|
||||
terminal.cols > 0 ? rect.width / terminal.cols : 10;
|
||||
|
||||
// Calculate actual menu height based on number of items
|
||||
// Each item is ~32px (py-1.5), footer is ~32px, max total 240px
|
||||
const itemHeight = 32;
|
||||
const footerHeight = 32;
|
||||
const maxMenuHeight = 240;
|
||||
@@ -1388,14 +1336,11 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
matches.length * itemHeight + footerHeight,
|
||||
maxMenuHeight,
|
||||
);
|
||||
|
||||
// Get cursor position in viewport coordinates
|
||||
const cursorBottomY = rect.top + (cursorY + 1) * cellHeight;
|
||||
const cursorTopY = rect.top + cursorY * cellHeight;
|
||||
const spaceBelow = window.innerHeight - cursorBottomY;
|
||||
const spaceAbove = cursorTopY;
|
||||
|
||||
// Show above cursor if not enough space below and more space above
|
||||
const showAbove =
|
||||
spaceBelow < estimatedMenuHeight && spaceAbove > spaceBelow;
|
||||
|
||||
@@ -1410,10 +1355,9 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
setShowAutocomplete(true);
|
||||
}
|
||||
}
|
||||
return false; // Prevent default Tab behavior
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let terminal handle all other keys
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1470,7 +1414,6 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't set isFitted to false - keep terminal visible during resize
|
||||
let rafId1: number;
|
||||
let rafId2: number;
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export function CommandAutocomplete({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const selectedRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Scroll selected item into view
|
||||
useEffect(() => {
|
||||
if (selectedRef.current && containerRef.current) {
|
||||
selectedRef.current.scrollIntoView({
|
||||
@@ -33,8 +32,6 @@ export function CommandAutocomplete({
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate max height for suggestions list to ensure footer is always visible
|
||||
// Footer height is approximately 32px (text + padding + border)
|
||||
const footerHeight = 32;
|
||||
const maxSuggestionsHeight = 240 - footerHeight;
|
||||
|
||||
@@ -62,9 +59,7 @@ export function CommandAutocomplete({
|
||||
index === selectedIndex && "bg-gray-500/20 text-gray-400",
|
||||
)}
|
||||
onClick={() => onSelect(suggestion)}
|
||||
onMouseEnter={() => {
|
||||
// Optional: update selected index on hover
|
||||
}}
|
||||
onMouseEnter={() => {}}
|
||||
>
|
||||
{suggestion}
|
||||
</div>
|
||||
|
||||
@@ -105,14 +105,12 @@ export function SSHToolsSidebar({
|
||||
};
|
||||
const [activeTab, setActiveTab] = useState(initialTab || "ssh-tools");
|
||||
|
||||
// Update active tab when initialTab changes
|
||||
useEffect(() => {
|
||||
if (initialTab && isOpen) {
|
||||
setActiveTab(initialTab);
|
||||
}
|
||||
}, [initialTab, isOpen]);
|
||||
|
||||
// Call onTabChange when active tab changes
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
if (onTabChange) {
|
||||
@@ -120,14 +118,12 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
};
|
||||
|
||||
// SSH Tools state
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
|
||||
const [rightClickCopyPaste, setRightClickCopyPaste] = useState<boolean>(
|
||||
() => getCookie("rightClickCopyPaste") === "true",
|
||||
);
|
||||
|
||||
// Snippets state
|
||||
const [snippets, setSnippets] = useState<Snippet[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
@@ -145,14 +141,12 @@ export function SSHToolsSidebar({
|
||||
[],
|
||||
);
|
||||
|
||||
// Command History state
|
||||
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
||||
const [isHistoryLoading, setIsHistoryLoading] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [historyRefreshCounter, setHistoryRefreshCounter] = useState(0);
|
||||
const commandHistoryScrollRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Split Screen state
|
||||
const [splitMode, setSplitMode] = useState<"none" | "2" | "3" | "4">("none");
|
||||
const [splitAssignments, setSplitAssignments] = useState<Map<number, number>>(
|
||||
new Map(),
|
||||
@@ -163,7 +157,6 @@ export function SSHToolsSidebar({
|
||||
null,
|
||||
);
|
||||
|
||||
// Resize state
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const startXRef = React.useRef<number | null>(null);
|
||||
const startWidthRef = React.useRef<number>(sidebarWidth);
|
||||
@@ -174,7 +167,6 @@ export function SSHToolsSidebar({
|
||||
activeUiTab?.type === "terminal" ? activeUiTab : undefined;
|
||||
const activeTerminalHostId = activeTerminal?.hostConfig?.id;
|
||||
|
||||
// Get splittable tabs (terminal, server, file_manager)
|
||||
const splittableTabs = tabs.filter(
|
||||
(tab: TabData) =>
|
||||
tab.type === "terminal" ||
|
||||
@@ -183,20 +175,16 @@ export function SSHToolsSidebar({
|
||||
tab.type === "user_profile",
|
||||
);
|
||||
|
||||
// Fetch command history
|
||||
useEffect(() => {
|
||||
if (isOpen && activeTab === "command-history") {
|
||||
if (activeTerminalHostId) {
|
||||
// Save current scroll position before any state updates
|
||||
const scrollTop = commandHistoryScrollRef.current?.scrollTop || 0;
|
||||
|
||||
getCommandHistory(activeTerminalHostId)
|
||||
.then((history) => {
|
||||
setCommandHistory((prevHistory) => {
|
||||
const newHistory = Array.isArray(history) ? history : [];
|
||||
// Only update if history actually changed
|
||||
if (JSON.stringify(prevHistory) !== JSON.stringify(newHistory)) {
|
||||
// Use requestAnimationFrame to restore scroll after React finishes rendering
|
||||
requestAnimationFrame(() => {
|
||||
if (commandHistoryScrollRef.current) {
|
||||
commandHistoryScrollRef.current.scrollTop = scrollTop;
|
||||
@@ -223,7 +211,6 @@ export function SSHToolsSidebar({
|
||||
historyRefreshCounter,
|
||||
]);
|
||||
|
||||
// Auto-refresh command history every 2 seconds when history tab is active
|
||||
useEffect(() => {
|
||||
if (isOpen && activeTab === "command-history" && activeTerminalHostId) {
|
||||
const refreshInterval = setInterval(() => {
|
||||
@@ -234,14 +221,12 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
}, [isOpen, activeTab, activeTerminalHostId]);
|
||||
|
||||
// Filter command history based on search query
|
||||
const filteredCommands = searchQuery
|
||||
? commandHistory.filter((cmd) =>
|
||||
cmd.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
: commandHistory;
|
||||
|
||||
// Initialize CSS variable on mount and when sidebar width changes
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
"--right-sidebar-width",
|
||||
@@ -249,7 +234,6 @@ export function SSHToolsSidebar({
|
||||
);
|
||||
}, [sidebarWidth]);
|
||||
|
||||
// Handle window resize to adjust sidebar width
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const minWidth = Math.min(300, Math.floor(window.innerWidth * 0.2));
|
||||
@@ -270,7 +254,6 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
}, [isOpen, activeTab]);
|
||||
|
||||
// Resize handlers
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsResizing(true);
|
||||
@@ -283,7 +266,7 @@ export function SSHToolsSidebar({
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (startXRef.current == null) return;
|
||||
const dx = startXRef.current - e.clientX; // Reversed because we're on the right
|
||||
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);
|
||||
@@ -295,13 +278,11 @@ export function SSHToolsSidebar({
|
||||
finalWidth = maxWidth;
|
||||
}
|
||||
|
||||
// Update CSS variable immediately for smooth animation
|
||||
document.documentElement.style.setProperty(
|
||||
"--right-sidebar-width",
|
||||
`${finalWidth}px`,
|
||||
);
|
||||
|
||||
// Update React state (this will be batched/debounced naturally)
|
||||
setSidebarWidth(finalWidth);
|
||||
};
|
||||
|
||||
@@ -323,7 +304,6 @@ export function SSHToolsSidebar({
|
||||
};
|
||||
}, [isResizing]);
|
||||
|
||||
// SSH Tools handlers
|
||||
const handleTabToggle = (tabId: number) => {
|
||||
setSelectedTabIds((prev) =>
|
||||
prev.includes(tabId)
|
||||
@@ -487,7 +467,6 @@ export function SSHToolsSidebar({
|
||||
setRightClickCopyPaste(checked);
|
||||
};
|
||||
|
||||
// Snippets handlers
|
||||
const fetchSnippets = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -599,15 +578,12 @@ export function SSHToolsSidebar({
|
||||
toast.success(t("snippets.copySuccess", { name: snippet.name }));
|
||||
};
|
||||
|
||||
// Split Screen handlers
|
||||
const handleSplitModeChange = (mode: "none" | "2" | "3" | "4") => {
|
||||
setSplitMode(mode);
|
||||
|
||||
if (mode === "none") {
|
||||
// Clear all splits
|
||||
handleClearSplit();
|
||||
} else {
|
||||
// Clear assignments when changing modes
|
||||
setSplitAssignments(new Map());
|
||||
setPreviewKey((prev) => prev + 1);
|
||||
}
|
||||
@@ -636,7 +612,6 @@ export function SSHToolsSidebar({
|
||||
|
||||
setSplitAssignments((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
// Remove this tab from any other cell
|
||||
Array.from(newMap.entries()).forEach(([idx, id]) => {
|
||||
if (id === draggedTabId && idx !== cellIndex) {
|
||||
newMap.delete(idx);
|
||||
@@ -677,7 +652,6 @@ export function SSHToolsSidebar({
|
||||
|
||||
const requiredSlots = parseInt(splitMode);
|
||||
|
||||
// Validate: All layout spots must be filled
|
||||
if (splitAssignments.size < requiredSlots) {
|
||||
toast.error(
|
||||
t("splitScreen.error.fillAllSlots", {
|
||||
@@ -688,7 +662,6 @@ export function SSHToolsSidebar({
|
||||
return;
|
||||
}
|
||||
|
||||
// Build ordered array of tab IDs based on cell index (0, 1, 2, 3)
|
||||
const orderedTabIds: number[] = [];
|
||||
for (let i = 0; i < requiredSlots; i++) {
|
||||
const tabId = splitAssignments.get(i);
|
||||
@@ -697,18 +670,15 @@ export function SSHToolsSidebar({
|
||||
}
|
||||
}
|
||||
|
||||
// First, clear ALL existing splits
|
||||
const currentSplits = [...allSplitScreenTab];
|
||||
currentSplits.forEach((tabId) => {
|
||||
setSplitScreenTab(tabId); // Toggle off
|
||||
setSplitScreenTab(tabId);
|
||||
});
|
||||
|
||||
// Then, add only the newly assigned tabs to split IN ORDER
|
||||
orderedTabIds.forEach((tabId) => {
|
||||
setSplitScreenTab(tabId); // Toggle on
|
||||
setSplitScreenTab(tabId);
|
||||
});
|
||||
|
||||
// Set first assigned tab as active if current tab is not in split
|
||||
if (!orderedTabIds.includes(currentTab ?? 0)) {
|
||||
setCurrentTab(orderedTabIds[0]);
|
||||
}
|
||||
@@ -721,7 +691,6 @@ export function SSHToolsSidebar({
|
||||
};
|
||||
|
||||
const handleClearSplit = () => {
|
||||
// Remove all tabs from split screen
|
||||
allSplitScreenTab.forEach((tabId) => {
|
||||
setSplitScreenTab(tabId);
|
||||
});
|
||||
@@ -741,7 +710,6 @@ export function SSHToolsSidebar({
|
||||
handleClearSplit();
|
||||
};
|
||||
|
||||
// Command History handlers
|
||||
const handleCommandSelect = (command: string) => {
|
||||
if (activeTerminal?.terminalRef?.current?.sendInput) {
|
||||
activeTerminal.terminalRef.current.sendInput(command);
|
||||
|
||||
Reference in New Issue
Block a user