fix: Command history and file manager styling issues

This commit is contained in:
LukeGus
2025-11-11 09:25:32 -06:00
parent 08aef18989
commit 26b71c0b69
3 changed files with 100 additions and 62 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -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);