Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions frontend/src/components/SessionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -973,7 +973,7 @@ export const SessionView = memo(() => {
.filter(p => !defaultTerminalPanel || p.id !== defaultTerminalPanel.id)
.map(panel => {
const isActive = panel.id === currentActivePanel.id;
const shouldKeepAlive = ['terminal', 'browser'].includes(panel.type);
const shouldKeepAlive = ['terminal', 'browser', 'diff', 'explorer'].includes(panel.type);
if (!isActive && !shouldKeepAlive) return null;
return (
<div
Expand Down Expand Up @@ -1116,7 +1116,7 @@ export const SessionView = memo(() => {
.filter(p => !defaultTerminalPanel || p.id !== defaultTerminalPanel.id)
.map(panel => {
const isActive = panel.id === currentActivePanel.id;
const shouldKeepAlive = ['terminal', 'browser'].includes(panel.type);
const shouldKeepAlive = ['terminal', 'browser', 'diff', 'explorer'].includes(panel.type);
if (!isActive && !shouldKeepAlive) return null;
return (
<div
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/panels/diff/CombinedDiffView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ const CombinedDiffView = memo(forwardRef<CombinedDiffViewHandle, CombinedDiffVie
// Background prefetch: when git status changes, fetch executions + diff even when not visible
// This ensures the diff tab has data ready before the user opens it
useEffect(() => {
if (!isVisible) return;
let cancelled = false;

const handleGitStatusUpdated = (event: Event) => {
Expand Down Expand Up @@ -346,7 +347,7 @@ const CombinedDiffView = memo(forwardRef<CombinedDiffViewHandle, CombinedDiffVie
cancelled = true;
window.removeEventListener('git-status-updated', handleGitStatusUpdated);
};
}, [sessionId, processExecutions]);
}, [sessionId, isVisible, processExecutions]);

// Keep refs to avoid stale closures in event handlers
const executionsLengthRef = useRef(executions.length);
Expand Down
25 changes: 24 additions & 1 deletion frontend/src/components/panels/diff/DiffPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import CombinedDiffView from './CombinedDiffView';
import type { CombinedDiffViewHandle } from './CombinedDiffView';
import type { ToolPanel, DiffPanelState } from '../../../../../shared/types/panels';
import { AlertCircle } from 'lucide-react';
import { useSession } from '../../../contexts/SessionContext';

const DIFF_PRELOAD_DELAY_MS = 10000;

interface DiffPanelProps {
panel: ToolPanel;
Expand All @@ -17,12 +20,32 @@ export const DiffPanel: React.FC<DiffPanelProps> = ({
sessionId,
isMainRepo = false
}) => {
const sessionContext = useSession();
const [isStale, setIsStale] = useState(false);
const [backgroundPreloadReady, setBackgroundPreloadReady] = useState(isActive);
const diffState = panel.state?.customState as DiffPanelState | undefined;
const lastRefreshRef = useRef<number>(Date.now());
const combinedDiffRef = useRef<CombinedDiffViewHandle>(null);
// Track diff-relevant git state to avoid spurious refreshes on no-op status events
const lastGitFingerprintRef = useRef<string>('');
const sessionStatus = sessionContext?.session.status;
const isSessionBusy = sessionStatus === 'running' || sessionStatus === 'initializing';
const shouldLoadDiff = isActive || (backgroundPreloadReady && !isSessionBusy);

useEffect(() => {
if (isActive) {
setBackgroundPreloadReady(true);
return;
}

if (backgroundPreloadReady || isSessionBusy) return;

const timer = window.setTimeout(() => {
setBackgroundPreloadReady(true);
}, DIFF_PRELOAD_DELAY_MS);

return () => window.clearTimeout(timer);
}, [isActive, backgroundPreloadReady, isSessionBusy]);

// Listen for file change events from other panels
useEffect(() => {
Expand Down Expand Up @@ -129,7 +152,7 @@ export const DiffPanel: React.FC<DiffPanelProps> = ({
selectedExecutions={[]}
isGitOperationRunning={false}
isMainRepo={isMainRepo}
isVisible={isActive}
isVisible={shouldLoadDiff}
/>
</div>
</div>
Expand Down
14 changes: 1 addition & 13 deletions frontend/src/components/panels/editor/EditorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,18 +151,6 @@ export const ExplorerPanel: React.FC<ExplorerPanelProps> = ({
}
}, [panel.id, handleStateChange]);

// Only render when active (for memory efficiency)
if (!isActive) {
return (
<div className="flex-1 flex items-center justify-center text-text-secondary">
<div className="text-center">
<div className="text-sm">Explorer panel not active</div>
<div className="text-xs mt-1 text-text-tertiary">Click to activate</div>
</div>
</div>
);
}

return (
<div className="h-full w-full">
<FileEditor
Expand All @@ -176,4 +164,4 @@ export const ExplorerPanel: React.FC<ExplorerPanelProps> = ({
);
};

export default ExplorerPanel;
export default ExplorerPanel;
84 changes: 77 additions & 7 deletions frontend/src/components/panels/editor/FileEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const PDF_EXTENSIONS = new Set(['pdf']);
interface HeadlessFileTreeProps {
sessionId: string;
onFileSelect: (file: FileItem | null) => void;
onFileCreateSelect?: (filePath: string) => void;
selectedPath: string | null;
initialExpandedDirs?: string[];
initialSearchQuery?: string;
Expand All @@ -42,6 +43,7 @@ interface HeadlessFileTreeProps {
function HeadlessFileTree({
sessionId,
onFileSelect,
onFileCreateSelect,
selectedPath,
initialExpandedDirs,
initialSearchQuery,
Expand Down Expand Up @@ -73,6 +75,7 @@ function HeadlessFileTree({
const [renamingPath, setRenamingPath] = useState<string | null>(null);
const [renamingValue, setRenamingValue] = useState('');
const skipRenameCommitRef = useRef(false);
const itemElementRefs = useRef(new Map<string, HTMLDivElement>());

// Context menu state
const [contextMenu, setContextMenu] = useState<{
Expand Down Expand Up @@ -158,6 +161,33 @@ function HeadlessFileTree({
filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')) : ''
), []);

const getAncestorDirs = useCallback((filePath: string) => {
const parts = filePath.split('/').filter(Boolean);
const ancestors: string[] = [];
for (let i = 1; i < parts.length; i++) {
ancestors.push(parts.slice(0, i).join('/'));
}
return ancestors;
}, []);

const revealItem = useCallback((filePath: string) => {
let attempts = 0;
const tryReveal = () => {
const element = itemElementRefs.current.get(filePath);
if (element) {
element.scrollIntoView({ block: 'nearest' });
return;
}

attempts += 1;
if (attempts < 10) {
window.setTimeout(tryReveal, 50);
}
};

window.setTimeout(tryReveal, 0);
}, []);

const refreshDirectory = useCallback((dirPath: string) => {
filesCacheRef.current.delete(dirPath);
tree.getItemInstance(dirPath || ROOT_ID)?.invalidateChildrenIds();
Expand All @@ -167,14 +197,22 @@ function HeadlessFileTree({
const dirs = new Set<string>(['']);
for (const filePath of paths) {
dirs.add(getParentPath(filePath));
getAncestorDirs(filePath).forEach(dir => dirs.add(dir));
filesCacheRef.current.delete(filePath);
const prefix = `${filePath}/`;
for (const key of filesCacheRef.current.keys()) {
if (key.startsWith(prefix)) filesCacheRef.current.delete(key);
}
}
dirs.forEach(refreshDirectory);
}, [getParentPath, refreshDirectory]);
}, [getParentPath, getAncestorDirs, refreshDirectory]);

useEffect(() => {
if (!selectedPath) return;
setSelectedItems([selectedPath]);
setExpandedItems(prev => Array.from(new Set([ROOT_ID, ...prev, ...getAncestorDirs(selectedPath)])));
revealItem(selectedPath);
}, [selectedPath, getAncestorDirs, revealItem]);

const getSelectedFilesForAction = useCallback((fallback: FileItem | null) => {
if (fallback && !selectedItems.includes(fallback.path)) return [fallback];
Expand Down Expand Up @@ -427,9 +465,16 @@ function HeadlessFileTree({
});

if (result.success) {
filesCacheRef.current.delete(newItemParentPath);
const parentItemId = newItemParentPath || ROOT_ID;
tree.getItemInstance(parentItemId)?.invalidateChildrenIds();
const createdItemPath = isFolder ? relativePath : filePath;
refreshAfterPathsChanged([createdItemPath]);
const dirsToExpand = [
ROOT_ID,
...getAncestorDirs(createdItemPath),
...(isFolder ? [relativePath] : []),
];
setExpandedItems(prev => Array.from(new Set([...prev, ...dirsToExpand])));
setSelectedItems([createdItemPath]);
revealItem(createdItemPath);

// AUTO-OPEN: Select and open the new file in editor — this is the bug fix
if (!isFolder) {
Expand All @@ -438,6 +483,7 @@ function HeadlessFileTree({
path: relativePath,
isDirectory: false,
};
onFileCreateSelect?.(newFile.path);
onFileSelect(newFile);
}

Expand All @@ -451,7 +497,7 @@ function HeadlessFileTree({
console.error('Failed to create item:', err);
setError(err instanceof Error ? err.message : 'Failed to create item');
}
}, [sessionId, newItemName, newItemParentPath, showNewItemDialog, onFileSelect, tree]);
}, [sessionId, newItemName, newItemParentPath, showNewItemDialog, onFileSelect, onFileCreateSelect, getAncestorDirs, revealItem, refreshAfterPathsChanged]);

// Refresh all
const handleRefreshAll = useCallback(() => {
Expand Down Expand Up @@ -872,14 +918,20 @@ function HeadlessFileTree({
const level = item.getItemMeta().level;
const isExpanded = item.isExpanded();
const isItemSelected = item.isSelected();
const isSelected = selectedPath === data.path || isItemSelected;
const isOpenFile = selectedPath === data.path && !isFolder;

return (
<div
key={item.getId()}
{...item.getProps()}
ref={(element) => {
if (element) itemElementRefs.current.set(data.path, element);
else itemElementRefs.current.delete(data.path);
}}
className={`flex items-center px-2 py-1 hover:bg-surface-hover cursor-pointer group ${
isSelected ? 'bg-interactive' : ''
isItemSelected ? 'bg-interactive' : ''
} ${
isOpenFile && !isItemSelected ? 'bg-surface-hover/60' : ''
} ${
dragOverPath === data.path ? 'ring-1 ring-interactive bg-interactive/10' : ''
}`}
Expand Down Expand Up @@ -1131,6 +1183,7 @@ export function FileEditor({
const binaryBlobUrlRef = useRef<string | null>(null);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<typeof monaco | null>(null);
const pendingEditorFocusPathRef = useRef<string | null>(null);

// Keep ref in sync and clean up blob URLs to prevent memory leaks
useEffect(() => {
Expand Down Expand Up @@ -1253,6 +1306,9 @@ export function FileEditor({
setOriginalContent(result.content);
setSelectedFile(file);
setViewMode('edit'); // Reset to edit mode when opening a new file
if (pendingEditorFocusPathRef.current === file.path) {
window.setTimeout(() => editorRef.current?.focus(), 100);
}

// Notify parent about file change
if (onFileChange) {
Expand Down Expand Up @@ -1315,6 +1371,17 @@ export function FileEditor({
}
}, [sessionId, onFileChange, onStateChange, initialState, binaryBlobUrlRef]);

const selectedFilePath = selectedFile?.path;

useEffect(() => {
if (!selectedFilePath || pendingEditorFocusPathRef.current !== selectedFilePath) return;
const focusTimer = window.setTimeout(() => {
editorRef.current?.focus();
pendingEditorFocusPathRef.current = null;
}, 100);
return () => window.clearTimeout(focusTimer);
}, [selectedFilePath]);


const handleEditorMount = (editor: monaco.editor.IStandaloneCodeEditor, monacoInstance: typeof monaco) => {
editorRef.current = editor;
Expand Down Expand Up @@ -1514,6 +1581,9 @@ export function FileEditor({
<HeadlessFileTree
sessionId={sessionId}
onFileSelect={loadFile}
onFileCreateSelect={(filePath) => {
pendingEditorFocusPathRef.current = filePath;
}}
selectedPath={selectedFile?.path || null}
initialExpandedDirs={initialState?.expandedDirs}
initialSearchQuery={initialState?.searchQuery}
Expand Down
Loading