fix: Fix tab reload/state loss whenever moving them to the rigbht
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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(
|
||||
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(() => {
|
||||
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();
|
||||
}, [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(() => {
|
||||
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">
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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,25 +404,20 @@ 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);
|
||||
}
|
||||
}
|
||||
// Immediately reset drag state after drop to ensure a single re-render
|
||||
// with updated tabs and cleared drag state.
|
||||
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,
|
||||
@@ -448,14 +425,31 @@ export function TopNavbar({
|
||||
currentX: 0,
|
||||
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 = () => {
|
||||
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,15 +527,10 @@ 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 = "";
|
||||
|
||||
// Skip all transforms if we just dropped to prevent glitches
|
||||
if (!isInDropAnimation) {
|
||||
if (isDraggingThisTab) {
|
||||
transform = `translateX(${dragOffset}px)`;
|
||||
} else if (
|
||||
@@ -578,10 +567,6 @@ export function TopNavbar({
|
||||
transform = `translateX(${draggedTabWidth + gap}px)`;
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostic log for transform
|
||||
if (dragState.draggedIndex !== null) {
|
||||
console.log(` Tab ID: ${tab.id}, Transform: ${transform}`);
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user