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);
return () => {
console.log(
`🔴 Terminal UNMOUNTING - this should NOT happen during drag!`,
);
isUnmountingRef.current = true;
shouldNotReconnectRef.current = true;
isReconnectingRef.current = false;
@@ -742,10 +745,21 @@ export const Terminal = forwardRef<TerminalHandle, SSHTerminalProps>(
}, [xtermRef, terminal]);
useEffect(() => {
console.log(`📡 Terminal connection useEffect triggered:`, {
terminal: !!terminal,
hostConfig: !!hostConfig,
visible,
isConnected,
isConnecting,
});
if (!terminal || !hostConfig || !visible) return;
if (isConnected || isConnecting) return;
console.log(
`🔌 Initiating NEW connection - this should only happen on mount!`,
);
setIsConnecting(true);
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 { Server as ServerView } from "@/ui/Desktop/Apps/Server/Server.tsx";
import { FileManager } from "@/ui/Desktop/Apps/File Manager/FileManager.tsx";
@@ -43,11 +43,15 @@ export function AppView({
};
const { state: sidebarState } = useSidebar();
const terminalTabs = tabs.filter(
(tab: TabData) =>
tab.type === "terminal" ||
tab.type === "server" ||
tab.type === "file_manager",
const terminalTabs = useMemo(
() =>
tabs.filter(
(tab: TabData) =>
tab.type === "terminal" ||
tab.type === "server" ||
tab.type === "file_manager",
),
[tabs],
);
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(() => {
hideThenFit();
}, [currentTab, terminalTabs.length, allSplitScreenTab.join(",")]);
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();
}
// Update the ref for next comparison
prevStateRef.current = {
terminalTabsLength: terminalTabs.length,
currentTab,
splitScreenTabsStr: allSplitScreenTab.join(","),
terminalTabIds: currentTabIds,
};
}, [
currentTab,
terminalTabs.length,
allSplitScreenTab.join(","),
terminalTabs,
]);
useEffect(() => {
scheduleMeasureAndFit();
@@ -138,6 +194,14 @@ export function AppView({
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 styles: Record<number, React.CSSProperties> = {};
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 (
<div className="absolute inset-0 z-[1]">
{terminalTabs.map((t: TabData) => {
{sortedTerminalTabs.map((t: TabData) => {
const hasStyle = !!styles[t.id];
const isVisible =
hasStyle || (allSplitScreenTab.length === 0 && t.id === currentTab);
@@ -202,6 +270,7 @@ export function AppView({
} as React.CSSProperties);
const effectiveVisible = isVisible && ready;
return (
<div key={t.id} style={finalStyle}>
<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);
};
const isReorderingRef = useRef(false);
const reorderTabs = (fromIndex: number, toIndex: number) => {
if (isReorderingRef.current) return;
isReorderingRef.current = true;
setTabs((prev) => {
const newTabs = [...prev];
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;
});
};

View File

@@ -1,4 +1,5 @@
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, Hammer, FileText } from "lucide-react";
@@ -58,7 +59,8 @@ export function TopNavbar({
const [isRecording, setIsRecording] = useState(false);
const [selectedTabIds, setSelectedTabIds] = useState<number[]>([]);
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<{
draggedId: number | null;
draggedIndex: number | null;
@@ -74,6 +76,7 @@ export function TopNavbar({
});
const containerRef = React.useRef<HTMLDivElement | null>(null);
const tabRefs = React.useRef<Map<number, HTMLDivElement>>(new Map());
const isProcessingDropRef = React.useRef(false);
const prevTabsRef = React.useRef<TabData[]>([]);
@@ -256,27 +259,9 @@ export function TopNavbar({
React.useEffect(() => {
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 = [];
}
}, [tabs]); // Depend only on tabs
}, [tabs]);
React.useEffect(() => {
if (justDroppedTabId !== null) {
@@ -286,9 +271,6 @@ export function TopNavbar({
}, [justDroppedTabId]);
const handleDragStart = (e: React.DragEvent, index: number) => {
console.log("Drag start:", index, e.clientX);
// Create transparent drag image
const img = new Image();
img.src =
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
@@ -304,11 +286,9 @@ export function TopNavbar({
};
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;
console.log("Dragging:", e.clientX);
setDragState((prev) => ({
...prev,
currentX: e.clientX,
@@ -370,7 +350,7 @@ export function TopNavbar({
// Moving right - find the rightmost tab whose midpoint we've passed
for (let i = draggedIndex + 1; i < tabBoundaries.length; i++) {
if (draggedCenter > tabBoundaries[i].mid) {
newTargetIndex = i; // Reverted from i + 1 to i
newTargetIndex = i;
} else {
break;
}
@@ -385,7 +365,9 @@ export function TopNavbar({
const containerRect = containerRef.current.getBoundingClientRect();
const lastTabEndInContainer = lastTabRect.right - containerRect.left;
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,40 +404,52 @@ export function TopNavbar({
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
console.log("Drop:", dragState);
if (
dragState.draggedIndex !== null &&
dragState.targetIndex !== null &&
dragState.draggedIndex !== dragState.targetIndex
) {
console.log("Tabs before reorder (IDs and references):");
tabs.forEach((tab, idx) =>
console.log(` [${idx}] ID: ${tab.id}, Ref:`, tab),
);
prevTabsRef.current = tabs; // Store current tabs before reorder
reorderTabs(dragState.draggedIndex, dragState.targetIndex);
if (dragState.draggedId !== null) {
setJustDroppedTabId(dragState.draggedId);
if (isProcessingDropRef.current) return;
isProcessingDropRef.current = true;
const fromIndex = dragState.draggedIndex;
const toIndex = dragState.targetIndex;
const draggedId = dragState.draggedId;
if (fromIndex !== null && toIndex !== null && fromIndex !== toIndex) {
prevTabsRef.current = tabs;
// Set animation flag and clear drag state synchronously
flushSync(() => {
setIsInDropAnimation(true);
setDragState({
draggedId: null,
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
});
reorderTabs(fromIndex, toIndex);
if (draggedId !== null) {
setJustDroppedTabId(draggedId);
}
} else {
setDragState({
draggedId: null,
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
}
// Immediately reset drag state after drop to ensure a single re-render
// with updated tabs and cleared drag state.
setDragState({
draggedId: null,
draggedIndex: null,
startX: 0,
currentX: 0,
targetIndex: null,
});
setTimeout(() => {
isProcessingDropRef.current = false;
setIsInDropAnimation(false);
}, 50);
};
const handleDragEnd = () => {
console.log("Drag end:", dragState);
// 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.
setIsInDropAnimation(false);
setDragState({
draggedId: null,
draggedIndex: null,
@@ -533,55 +527,46 @@ export function TopNavbar({
? dragState.currentX - dragState.startX
: 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 = "";
if (isDraggingThisTab) {
transform = `translateX(${dragOffset}px)`;
} else if (
dragState.draggedIndex !== null &&
dragState.targetIndex !== null
) {
const draggedOriginalIndex = dragState.draggedIndex;
const currentTargetIndex = dragState.targetIndex;
// Determine if this tab should shift left or right
if (
draggedOriginalIndex < currentTargetIndex && // Dragging rightwards
index > draggedOriginalIndex && // This tab is to the right of the original position
index <= currentTargetIndex // This tab is at or before the target position
) {
// Shift left to make space
const draggedTabWidth =
tabRefs.current
.get(draggedOriginalIndex)
?.getBoundingClientRect().width || 0;
const gap = 4;
transform = `translateX(-${draggedTabWidth + gap}px)`;
// Skip all transforms if we just dropped to prevent glitches
if (!isInDropAnimation) {
if (isDraggingThisTab) {
transform = `translateX(${dragOffset}px)`;
} else if (
draggedOriginalIndex > currentTargetIndex && // Dragging leftwards
index >= currentTargetIndex && // This tab is at or after the target position
index < draggedOriginalIndex // This tab is to the left of the original position
dragState.draggedIndex !== null &&
dragState.targetIndex !== null
) {
// Shift right to make space
const draggedTabWidth =
tabRefs.current
.get(draggedOriginalIndex)
?.getBoundingClientRect().width || 0;
const gap = 4;
transform = `translateX(${draggedTabWidth + gap}px)`;
}
}
const draggedOriginalIndex = dragState.draggedIndex;
const currentTargetIndex = dragState.targetIndex;
// Diagnostic log for transform
if (dragState.draggedIndex !== null) {
console.log(` Tab ID: ${tab.id}, Transform: ${transform}`);
// Determine if this tab should shift left or right
if (
draggedOriginalIndex < currentTargetIndex && // Dragging rightwards
index > draggedOriginalIndex && // This tab is to the right of the original position
index <= currentTargetIndex // This tab is at or before the target position
) {
// Shift left to make space
const draggedTabWidth =
tabRefs.current
.get(draggedOriginalIndex)
?.getBoundingClientRect().width || 0;
const gap = 4;
transform = `translateX(-${draggedTabWidth + gap}px)`;
} else if (
draggedOriginalIndex > currentTargetIndex && // Dragging leftwards
index >= currentTargetIndex && // This tab is at or after the target position
index < draggedOriginalIndex // This tab is to the left of the original position
) {
// Shift right to make space
const draggedTabWidth =
tabRefs.current
.get(draggedOriginalIndex)
?.getBoundingClientRect().width || 0;
const gap = 4;
transform = `translateX(${draggedTabWidth + gap}px)`;
}
}
}
return (
@@ -606,7 +591,9 @@ export function TopNavbar({
style={{
transform,
transition:
isDraggingThisTab || isDroppedAndSnapping
isDraggingThisTab ||
isDroppedAndSnapping ||
isInDropAnimation
? "none"
: "transform 200ms ease-out",
zIndex: isDraggingThisTab ? 1000 : 1,