fix: Fix tab reload/state loss whenever moving them to the rigbht

This commit is contained in:
LukeGus
2025-10-22 11:47:02 -05:00
parent 471e2ff3fa
commit 0e8beaaa64
4 changed files with 196 additions and 111 deletions

View File

@@ -720,6 +720,9 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
setVisible(true); setVisible(true);
return () => { return () => {
console.log(
`🔴 Terminal UNMOUNTING - this should NOT happen during drag!`,
);
isUnmountingRef.current = true; isUnmountingRef.current = true;
shouldNotReconnectRef.current = true; shouldNotReconnectRef.current = true;
isReconnectingRef.current = false; isReconnectingRef.current = false;
@@ -742,10 +745,21 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
}, [xtermRef, terminal]); }, [xtermRef, terminal]);
useEffect(() => { useEffect(() => {
console.log(`📡 Terminal connection useEffect triggered:`, {
terminal: !!terminal,
hostConfig: !!hostConfig,
visible,
isConnected,
isConnecting,
});
if (!terminal || !hostConfig || !visible) return; if (!terminal || !hostConfig || !visible) return;
if (isConnected || isConnecting) return; if (isConnected || isConnecting) return;
console.log(
`🔌 Initiating NEW connection - this should only happen on mount!`,
);
setIsConnecting(true); setIsConnecting(true);
const readyFonts = const readyFonts =

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState, useMemo } from "react";
import { Terminal } from "@/ui/Desktop/Apps/Terminal/Terminal.tsx"; import { Terminal } from "@/ui/Desktop/Apps/Terminal/Terminal.tsx";
import { Server as ServerView } from "@/ui/Desktop/Apps/Server/Server.tsx"; import { Server as ServerView } from "@/ui/Desktop/Apps/Server/Server.tsx";
import { FileManager } from "@/ui/Desktop/Apps/File Manager/FileManager.tsx"; import { FileManager } from "@/ui/Desktop/Apps/File Manager/FileManager.tsx";
@@ -43,11 +43,15 @@ export function AppView({
}; };
const { state: sidebarState } = useSidebar(); const { state: sidebarState } = useSidebar();
const terminalTabs = tabs.filter( const terminalTabs = useMemo(
() =>
tabs.filter(
(tab: TabData) => (tab: TabData) =>
tab.type === "terminal" || tab.type === "terminal" ||
tab.type === "server" || tab.type === "server" ||
tab.type === "file_manager", tab.type === "file_manager",
),
[tabs],
); );
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
@@ -107,9 +111,61 @@ export function AppView({
}); });
}; };
const prevStateRef = useRef({
terminalTabsLength: terminalTabs.length,
currentTab,
splitScreenTabsStr: allSplitScreenTab.join(","),
terminalTabIds: terminalTabs.map((t) => t.id).join(","),
});
useEffect(() => { useEffect(() => {
const prev = prevStateRef.current;
const currentTabIds = terminalTabs.map((t) => t.id).join(",");
const lengthChanged = prev.terminalTabsLength !== terminalTabs.length;
const currentTabChanged = prev.currentTab !== currentTab;
const splitChanged =
prev.splitScreenTabsStr !== allSplitScreenTab.join(",");
const tabIdsChanged = prev.terminalTabIds !== currentTabIds;
// Only trigger hideThenFit if tabs were added/removed (not just reordered)
// or if current tab or split screen changed
const isJustReorder =
!lengthChanged && tabIdsChanged && !currentTabChanged && !splitChanged;
console.log("AppView useEffect:", {
lengthChanged,
currentTabChanged,
splitChanged,
tabIdsChanged,
isJustReorder,
willCallHideThenFit:
(lengthChanged || currentTabChanged || splitChanged) && !isJustReorder,
});
if (
(lengthChanged || currentTabChanged || splitChanged) &&
!isJustReorder
) {
console.log(
"CALLING hideThenFit - this will set ready=false and cause Terminal isVisible to become false!",
);
hideThenFit(); hideThenFit();
}, [currentTab, terminalTabs.length, allSplitScreenTab.join(",")]); }
// Update the ref for next comparison
prevStateRef.current = {
terminalTabsLength: terminalTabs.length,
currentTab,
splitScreenTabsStr: allSplitScreenTab.join(","),
terminalTabIds: currentTabIds,
};
}, [
currentTab,
terminalTabs.length,
allSplitScreenTab.join(","),
terminalTabs,
]);
useEffect(() => { useEffect(() => {
scheduleMeasureAndFit(); scheduleMeasureAndFit();
@@ -138,6 +194,14 @@ export function AppView({
const HEADER_H = 28; const HEADER_H = 28;
// Create a stable map of terminal IDs to preserve component identity
const terminalIdMapRef = useRef<Set<number>>(new Set());
// Track all terminal IDs that have ever existed
useEffect(() => {
terminalTabs.forEach((t) => terminalIdMapRef.current.add(t.id));
}, [terminalTabs]);
const renderTerminalsLayer = () => { const renderTerminalsLayer = () => {
const styles: Record<number, React.CSSProperties> = {}; const styles: Record<number, React.CSSProperties> = {};
const splitTabs = terminalTabs.filter((tab: TabData) => const splitTabs = terminalTabs.filter((tab: TabData) =>
@@ -184,9 +248,13 @@ export function AppView({
}); });
} }
// Render in a STABLE order by ID to prevent React from unmounting
// Sort by ID instead of array position
const sortedTerminalTabs = [...terminalTabs].sort((a, b) => a.id - b.id);
return ( return (
<div className="absolute inset-0 z-[1]"> <div className="absolute inset-0 z-[1]">
{terminalTabs.map((t: TabData) => { {sortedTerminalTabs.map((t: TabData) => {
const hasStyle = !!styles[t.id]; const hasStyle = !!styles[t.id];
const isVisible = const isVisible =
hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab); hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab);
@@ -202,6 +270,7 @@ export function AppView({
} as React.CSSProperties); } as React.CSSProperties);
const effectiveVisible = isVisible && ready; const effectiveVisible = isVisible && ready;
return ( return (
<div key={t.id} style={finalStyle}> <div key={t.id} style={finalStyle}>
<div className="absolute inset-0 rounded-md bg-dark-bg"> <div className="absolute inset-0 rounded-md bg-dark-bg">

View File

@@ -153,11 +153,26 @@ export function TabProvider({ children }: TabProviderProps) {
return tabs.find((tab) => tab.id === tabId); return tabs.find((tab) => tab.id === tabId);
}; };
const isReorderingRef = useRef(false);
const reorderTabs = (fromIndex: number, toIndex: number) => { const reorderTabs = (fromIndex: number, toIndex: number) => {
if (isReorderingRef.current) return;
isReorderingRef.current = true;
setTabs((prev) => { setTabs((prev) => {
const newTabs = [...prev]; const newTabs = [...prev];
const [movedTab] = newTabs.splice(fromIndex, 1); const [movedTab] = newTabs.splice(fromIndex, 1);
newTabs.splice(toIndex, 0, movedTab);
const maxIndex = newTabs.length;
const safeToIndex = Math.min(toIndex, maxIndex);
newTabs.splice(safeToIndex, 0, movedTab);
setTimeout(() => {
isReorderingRef.current = false;
}, 100);
return newTabs; return newTabs;
}); });
}; };

View File

@@ -1,4 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { flushSync } from "react-dom";
import { useSidebar } from "@/components/ui/sidebar.tsx"; import { useSidebar } from "@/components/ui/sidebar.tsx";
import { Button } from "@/components/ui/button.tsx"; import { Button } from "@/components/ui/button.tsx";
import { ChevronDown, ChevronUpIcon, Hammer, FileText } from "lucide-react"; import { ChevronDown, ChevronUpIcon, Hammer, FileText } from "lucide-react";
@@ -58,7 +59,8 @@ export function TopNavbar({
const [isRecording, setIsRecording] = useState(false); const [isRecording, setIsRecording] = useState(false);
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]); const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
const [snippetsSidebarOpen, setSnippetsSidebarOpen] = useState(false); const [snippetsSidebarOpen, setSnippetsSidebarOpen] = useState(false);
const [justDroppedTabId, setJustDroppedTabId] = useState<number | null>(null); // New state variable const [justDroppedTabId, setJustDroppedTabId] = useState<number | null>(null);
const [isInDropAnimation, setIsInDropAnimation] = useState(false);
const [dragState, setDragState] = useState<{ const [dragState, setDragState] = useState<{
draggedId: number | null; draggedId: number | null;
draggedIndex: number | null; draggedIndex: number | null;
@@ -74,6 +76,7 @@ export function TopNavbar({
}); });
const containerRef = React.useRef<HTMLDivElement | null>(null); const containerRef = React.useRef<HTMLDivElement | null>(null);
const tabRefs = React.useRef<Map<number, HTMLDivElement>>(new Map()); const tabRefs = React.useRef<Map<number, HTMLDivElement>>(new Map());
const isProcessingDropRef = React.useRef(false);
const prevTabsRef = React.useRef<TabData[]>([]); const prevTabsRef = React.useRef<TabData[]>([]);
@@ -256,27 +259,9 @@ export function TopNavbar({
React.useEffect(() => { React.useEffect(() => {
if (prevTabsRef.current.length > 0 && tabs !== prevTabsRef.current) { if (prevTabsRef.current.length > 0 && tabs !== prevTabsRef.current) {
// Check if tabs actually changed
console.log("Tabs AFTER reorder (IDs and references):");
tabs.forEach((newTab, newIdx) => {
const oldTab = prevTabsRef.current.find((t) => t.id === newTab.id);
console.log(
` [${newIdx}] ID: ${newTab.id}, Ref:`,
newTab,
`(Old Ref:`,
oldTab,
`)`,
);
if (oldTab && oldTab !== newTab) {
console.warn(` Tab ID ${newTab.id} object reference CHANGED!`);
} else if (oldTab && oldTab === newTab) {
console.info(` Tab ID ${newTab.id} object reference PRESERVED.`);
}
});
// Clear prevTabsRef.current only after the comparison is done
prevTabsRef.current = []; prevTabsRef.current = [];
} }
}, [tabs]); // Depend only on tabs }, [tabs]);
React.useEffect(() => { React.useEffect(() => {
if (justDroppedTabId !== null) { if (justDroppedTabId !== null) {
@@ -286,9 +271,6 @@ export function TopNavbar({
}, [justDroppedTabId]); }, [justDroppedTabId]);
const handleDragStart = (e: React.DragEvent, index: number) => { const handleDragStart = (e: React.DragEvent, index: number) => {
console.log("Drag start:", index, e.clientX);
// Create transparent drag image
const img = new Image(); const img = new Image();
img.src = img.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
@@ -304,11 +286,9 @@ export function TopNavbar({
}; };
const handleDrag = (e: React.DragEvent) => { const handleDrag = (e: React.DragEvent) => {
if (e.clientX === 0) return; // Skip the final drag event if (e.clientX === 0) return;
if (dragState.draggedIndex === null) return; if (dragState.draggedIndex === null) return;
console.log("Dragging:", e.clientX);
setDragState((prev) => ({ setDragState((prev) => ({
...prev, ...prev,
currentX: e.clientX, currentX: e.clientX,
@@ -370,7 +350,7 @@ export function TopNavbar({
// Moving right - find the rightmost tab whose midpoint we've passed // Moving right - find the rightmost tab whose midpoint we've passed
for (let i = draggedIndex + 1; i < tabBoundaries.length; i++) { for (let i = draggedIndex + 1; i < tabBoundaries.length; i++) {
if (draggedCenter > tabBoundaries[i].mid) { if (draggedCenter > tabBoundaries[i].mid) {
newTargetIndex = i; // Reverted from i + 1 to i newTargetIndex = i;
} else { } else {
break; break;
} }
@@ -385,7 +365,9 @@ export function TopNavbar({
const containerRect = containerRef.current.getBoundingClientRect(); const containerRect = containerRef.current.getBoundingClientRect();
const lastTabEndInContainer = lastTabRect.right - containerRect.left; const lastTabEndInContainer = lastTabRect.right - containerRect.left;
if (currentX > lastTabEndInContainer) { if (currentX > lastTabEndInContainer) {
newTargetIndex = tabBoundaries.length; // Insert at the very end // When dragging past the last tab, insert at the very end
// Use the last valid index (length - 1) not length itself
newTargetIndex = lastTabIndex;
} }
} }
} }
@@ -422,25 +404,20 @@ export function TopNavbar({
const handleDrop = (e: React.DragEvent) => { const handleDrop = (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
console.log("Drop:", dragState);
if ( if (isProcessingDropRef.current) return;
dragState.draggedIndex !== null && isProcessingDropRef.current = true;
dragState.targetIndex !== null &&
dragState.draggedIndex !== dragState.targetIndex const fromIndex = dragState.draggedIndex;
) { const toIndex = dragState.targetIndex;
console.log("Tabs before reorder (IDs and references):"); const draggedId = dragState.draggedId;
tabs.forEach((tab, idx) =>
console.log(` [${idx}] ID: ${tab.id}, Ref:`, tab), if (fromIndex !== null && toIndex !== null && fromIndex !== toIndex) {
); prevTabsRef.current = tabs;
prevTabsRef.current = tabs; // Store current tabs before reorder
reorderTabs(dragState.draggedIndex, dragState.targetIndex); // Set animation flag and clear drag state synchronously
if (dragState.draggedId !== null) { flushSync(() => {
setJustDroppedTabId(dragState.draggedId); setIsInDropAnimation(true);
}
}
// Immediately reset drag state after drop to ensure a single re-render
// with updated tabs and cleared drag state.
setDragState({ setDragState({
draggedId: null, draggedId: null,
draggedIndex: null, draggedIndex: null,
@@ -448,14 +425,31 @@ export function TopNavbar({
currentX: 0, currentX: 0,
targetIndex: null, targetIndex: null,
}); });
});
reorderTabs(fromIndex, toIndex);
if (draggedId !== null) {
setJustDroppedTabId(draggedId);
}
} else {
setDragState({
draggedId: null,
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
}
setTimeout(() => {
isProcessingDropRef.current = false;
setIsInDropAnimation(false);
}, 50);
}; };
const handleDragEnd = () => { const handleDragEnd = () => {
console.log("Drag end:", dragState); setIsInDropAnimation(false);
// Immediately reset drag state. If a drop occurred, handleDrop has already
// initiated the state clear. If the drag was cancelled (e.g., dropped
// outside a valid target), this clears the drag state.
setDragState({ setDragState({
draggedId: null, draggedId: null,
draggedIndex: null, draggedIndex: null,
@@ -533,15 +527,10 @@ export function TopNavbar({
? dragState.currentX - dragState.startX ? dragState.currentX - dragState.startX
: 0; : 0;
// Diagnostic logs
if (dragState.draggedIndex !== null) {
console.log(
`Tab ID: ${tab.id}, Index: ${index}, isDraggingThisTab: ${isDraggingThisTab}, draggedOriginalIndex: ${dragState.draggedIndex}, currentTargetIndex: ${dragState.targetIndex}, isDroppedAndSnapping: ${isDroppedAndSnapping}`,
);
}
// Calculate transform
let transform = ""; let transform = "";
// Skip all transforms if we just dropped to prevent glitches
if (!isInDropAnimation) {
if (isDraggingThisTab) { if (isDraggingThisTab) {
transform = `translateX(${dragOffset}px)`; transform = `translateX(${dragOffset}px)`;
} else if ( } else if (
@@ -578,10 +567,6 @@ export function TopNavbar({
transform = `translateX(${draggedTabWidth + gap}px)`; transform = `translateX(${draggedTabWidth + gap}px)`;
} }
} }
// Diagnostic log for transform
if (dragState.draggedIndex !== null) {
console.log(` Tab ID: ${tab.id}, Transform: ${transform}`);
} }
return ( return (
@@ -606,7 +591,9 @@ export function TopNavbar({
style={{ style={{
transform, transform,
transition: transition:
isDraggingThisTab || isDroppedAndSnapping isDraggingThisTab ||
isDroppedAndSnapping ||
isInDropAnimation
? "none" ? "none"
: "transform 200ms ease-out", : "transform 200ms ease-out",
zIndex: isDraggingThisTab ? 1000 : 1, zIndex: isDraggingThisTab ? 1000 : 1,