fix: Command history and file manager styling issues
This commit is contained in:
@@ -503,8 +503,6 @@ export function FileManagerContextMenu({
|
|||||||
data-context-menu
|
data-context-menu
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed bg-dark-bg border border-dark-border rounded-lg shadow-xl min-w-[180px] max-w-[250px] z-[99995] overflow-hidden",
|
"fixed bg-dark-bg border border-dark-border rounded-lg shadow-xl min-w-[180px] max-w-[250px] z-[99995] overflow-hidden",
|
||||||
"transition-all duration-150 ease-out origin-top-left",
|
|
||||||
isMounted ? "opacity-100 scale-100" : "opacity-0 scale-95",
|
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
left: menuPosition.x,
|
left: menuPosition.x,
|
||||||
|
|||||||
@@ -180,28 +180,44 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
||||||
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
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 =
|
||||||
|
commandHistoryContext.setCommandHistory;
|
||||||
|
}, [
|
||||||
|
commandHistoryContext.setIsLoading,
|
||||||
|
commandHistoryContext.setCommandHistory,
|
||||||
|
]);
|
||||||
|
|
||||||
// Load command history when dialog opens
|
// Load command history when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showHistoryDialog && hostConfig.id) {
|
if (showHistoryDialog && hostConfig.id) {
|
||||||
setIsLoadingHistory(true);
|
setIsLoadingHistory(true);
|
||||||
commandHistoryContext.setIsLoading(true);
|
setIsLoadingRef.current(true);
|
||||||
import("@/ui/main-axios.ts")
|
import("@/ui/main-axios.ts")
|
||||||
.then((module) => module.getCommandHistory(hostConfig.id!))
|
.then((module) => module.getCommandHistory(hostConfig.id!))
|
||||||
.then((history) => {
|
.then((history) => {
|
||||||
setCommandHistory(history);
|
setCommandHistory(history);
|
||||||
commandHistoryContext.setCommandHistory(history);
|
setCommandHistoryContextRef.current(history);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Failed to load command history:", error);
|
console.error("Failed to load command history:", error);
|
||||||
setCommandHistory([]);
|
setCommandHistory([]);
|
||||||
commandHistoryContext.setCommandHistory([]);
|
setCommandHistoryContextRef.current([]);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoadingHistory(false);
|
setIsLoadingHistory(false);
|
||||||
commandHistoryContext.setIsLoading(false);
|
setIsLoadingRef.current(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [showHistoryDialog, hostConfig.id, commandHistoryContext]);
|
}, [showHistoryDialog, hostConfig.id]);
|
||||||
|
|
||||||
// Load command history for autocomplete on mount (Stage 3)
|
// Load command history for autocomplete on mount (Stage 3)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -906,7 +922,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
// Register handlers with context
|
// Register handlers with context
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
commandHistoryContext.setOnSelectCommand(handleSelectCommand);
|
commandHistoryContext.setOnSelectCommand(handleSelectCommand);
|
||||||
}, [handleSelectCommand, commandHistoryContext]);
|
}, [handleSelectCommand]);
|
||||||
|
|
||||||
// Handle autocomplete selection (mouse click)
|
// Handle autocomplete selection (mouse click)
|
||||||
const handleAutocompleteSelect = useCallback(
|
const handleAutocompleteSelect = useCallback(
|
||||||
@@ -956,7 +972,7 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
// Update local state
|
// Update local state
|
||||||
setCommandHistory((prev) => {
|
setCommandHistory((prev) => {
|
||||||
const newHistory = prev.filter((cmd) => cmd !== command);
|
const newHistory = prev.filter((cmd) => cmd !== command);
|
||||||
commandHistoryContext.setCommandHistory(newHistory);
|
setCommandHistoryContextRef.current(newHistory);
|
||||||
return newHistory;
|
return newHistory;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -970,13 +986,13 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
console.error("Failed to delete command from history:", error);
|
console.error("Failed to delete command from history:", error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[hostConfig.id, commandHistoryContext],
|
[hostConfig.id],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Register delete handler with context
|
// Register delete handler with context
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
commandHistoryContext.setOnDeleteCommand(handleDeleteCommand);
|
commandHistoryContext.setOnDeleteCommand(handleDeleteCommand);
|
||||||
}, [handleDeleteCommand, commandHistoryContext]);
|
}, [handleDeleteCommand]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!terminal || !xtermRef.current) return;
|
if (!terminal || !xtermRef.current) return;
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ export function SSHToolsSidebar({
|
|||||||
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
const [commandHistory, setCommandHistory] = useState<string[]>([]);
|
||||||
const [isHistoryLoading, setIsHistoryLoading] = useState(false);
|
const [isHistoryLoading, setIsHistoryLoading] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [historyRefreshCounter, setHistoryRefreshCounter] = useState(0);
|
||||||
|
const commandHistoryScrollRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Resize state
|
// Resize state
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
@@ -150,25 +152,54 @@ export function SSHToolsSidebar({
|
|||||||
activeUiTab?.type === "terminal" ? activeUiTab : undefined;
|
activeUiTab?.type === "terminal" ? activeUiTab : undefined;
|
||||||
const activeTerminalHostId = activeTerminal?.hostConfig?.id;
|
const activeTerminalHostId = activeTerminal?.hostConfig?.id;
|
||||||
|
|
||||||
|
// Fetch command history
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && activeTab === "command-history") {
|
if (isOpen && activeTab === "command-history") {
|
||||||
if (activeTerminalHostId) {
|
if (activeTerminalHostId) {
|
||||||
setIsHistoryLoading(true);
|
// Save current scroll position before any state updates
|
||||||
|
const scrollTop = commandHistoryScrollRef.current?.scrollTop || 0;
|
||||||
|
|
||||||
getCommandHistory(activeTerminalHostId)
|
getCommandHistory(activeTerminalHostId)
|
||||||
.then((history) => {
|
.then((history) => {
|
||||||
setCommandHistory(history);
|
setCommandHistory((prevHistory) => {
|
||||||
|
// Only update if history actually changed
|
||||||
|
if (JSON.stringify(prevHistory) !== JSON.stringify(history)) {
|
||||||
|
// Use requestAnimationFrame to restore scroll after React finishes rendering
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (commandHistoryScrollRef.current) {
|
||||||
|
commandHistoryScrollRef.current.scrollTop = scrollTop;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
return prevHistory;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Failed to fetch command history", err);
|
console.error("Failed to fetch command history", err);
|
||||||
setCommandHistory([]);
|
setCommandHistory([]);
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsHistoryLoading(false);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setCommandHistory([]);
|
setCommandHistory([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
|
isOpen,
|
||||||
|
activeTab,
|
||||||
|
activeTerminalHostId,
|
||||||
|
currentTab,
|
||||||
|
historyRefreshCounter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Auto-refresh command history every 2 seconds when history tab is active
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && activeTab === "command-history" && activeTerminalHostId) {
|
||||||
|
const refreshInterval = setInterval(() => {
|
||||||
|
setHistoryRefreshCounter((prev) => prev + 1);
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
return () => clearInterval(refreshInterval);
|
||||||
|
}
|
||||||
}, [isOpen, activeTab, activeTerminalHostId]);
|
}, [isOpen, activeTab, activeTerminalHostId]);
|
||||||
|
|
||||||
// Filter command history based on search query
|
// Filter command history based on search query
|
||||||
@@ -543,32 +574,23 @@ export function SSHToolsSidebar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCommandDelete = (command: string) => {
|
const handleCommandDelete = async (command: string) => {
|
||||||
if (activeTerminalHostId) {
|
if (activeTerminalHostId) {
|
||||||
confirmWithToast(
|
try {
|
||||||
t("commandHistory.deleteConfirmDescription", {
|
await deleteCommandFromHistory(activeTerminalHostId, command);
|
||||||
defaultValue: `Delete "${command}" from history?`,
|
setCommandHistory((prev) => prev.filter((c) => c !== command));
|
||||||
command,
|
toast.success(
|
||||||
}),
|
t("commandHistory.deleteSuccess", {
|
||||||
async () => {
|
defaultValue: "Command deleted from history",
|
||||||
try {
|
}),
|
||||||
await deleteCommandFromHistory(activeTerminalHostId, command);
|
);
|
||||||
setCommandHistory((prev) => prev.filter((c) => c !== command));
|
} catch {
|
||||||
toast.success(
|
toast.error(
|
||||||
t("commandHistory.deleteSuccess", {
|
t("commandHistory.deleteFailed", {
|
||||||
defaultValue: "Command deleted from history",
|
defaultValue: "Failed to delete command.",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch {
|
}
|
||||||
toast.error(
|
|
||||||
t("commandHistory.deleteFailed", {
|
|
||||||
defaultValue: "Failed to delete command.",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"destructive",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -612,9 +634,13 @@ export function SSHToolsSidebar({
|
|||||||
</SidebarGroupLabel>
|
</SidebarGroupLabel>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<Separator className="p-0.25" />
|
<Separator className="p-0.25" />
|
||||||
<SidebarContent className="p-4">
|
<SidebarContent className="p-4 flex flex-col overflow-hidden">
|
||||||
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
<Tabs
|
||||||
<TabsList className="w-full grid grid-cols-3 mb-4">
|
value={activeTab}
|
||||||
|
onValueChange={handleTabChange}
|
||||||
|
className="flex flex-col h-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<TabsList className="w-full grid grid-cols-3 mb-4 flex-shrink-0">
|
||||||
<TabsTrigger value="ssh-tools">
|
<TabsTrigger value="ssh-tools">
|
||||||
{t("sshTools.title")}
|
{t("sshTools.title")}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -896,8 +922,11 @@ export function SSHToolsSidebar({
|
|||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="command-history" className="space-y-4">
|
<TabsContent
|
||||||
<div className="space-y-2">
|
value="command-history"
|
||||||
|
className="flex flex-col flex-1 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="space-y-2 flex-shrink-0 mb-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
@@ -923,17 +952,8 @@ export function SSHToolsSidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden">
|
<div className="flex-1 overflow-hidden min-h-0">
|
||||||
{isHistoryLoading ? (
|
{!activeTerminal ? (
|
||||||
<div className="flex flex-row items-center justify-center text-muted-foreground text-sm animate-pulse py-8">
|
|
||||||
<Loader2 className="animate-spin mr-2" size={16} />
|
|
||||||
<span>
|
|
||||||
{t("commandHistory.loading", {
|
|
||||||
defaultValue: "Loading history...",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : !activeTerminal ? (
|
|
||||||
<div className="text-center text-muted-foreground py-8">
|
<div className="text-center text-muted-foreground py-8">
|
||||||
<Terminal className="h-12 w-12 mb-4 opacity-20 mx-auto" />
|
<Terminal className="h-12 w-12 mb-4 opacity-20 mx-auto" />
|
||||||
<p className="mb-2 font-medium">
|
<p className="mb-2 font-medium">
|
||||||
@@ -982,23 +1002,27 @@ export function SSHToolsSidebar({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2 overflow-y-auto max-h-[calc(100vh-280px)]">
|
<div
|
||||||
|
ref={commandHistoryScrollRef}
|
||||||
|
className="space-y-2 overflow-y-auto h-full"
|
||||||
|
>
|
||||||
{filteredCommands.map((command, index) => (
|
{filteredCommands.map((command, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="bg-dark-bg border-2 border-dark-border rounded-md px-3 py-2.5 hover:bg-dark-hover-alt hover:border-blue-400/50 transition-all duration-200 group"
|
className="bg-dark-bg border-2 border-dark-border rounded-md px-3 py-2.5 hover:bg-dark-hover-alt hover:border-gray-600 transition-all duration-200 group h-12 flex items-center"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
||||||
<span
|
<span
|
||||||
className="flex-1 font-mono text-sm cursor-pointer text-white"
|
className="flex-1 font-mono text-sm cursor-pointer text-white truncate"
|
||||||
onClick={() => handleCommandSelect(command)}
|
onClick={() => handleCommandSelect(command)}
|
||||||
|
title={command}
|
||||||
>
|
>
|
||||||
{command}
|
{command}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 hover:bg-red-500/20 hover:text-red-400"
|
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) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleCommandDelete(command);
|
handleCommandDelete(command);
|
||||||
|
|||||||
Reference in New Issue
Block a user