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
12 changes: 3 additions & 9 deletions frontend/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,7 @@ function App() {
setGitPanelOpen(false);
} else {
// Sync store state when closing - this prevents the effect from re-opening
const focusedId = focusedSessionIdRef.current;
if (focusedId) {
useFileEditorSidebarStore.getState().setOpen(focusedId, false);
}
useFileEditorSidebarStore.getState().setOpen(false);
}
setFileEditorPanelOpen(open);
}, []);
Expand Down Expand Up @@ -140,9 +137,7 @@ function App() {

// Subscribe to file editor sidebar store to sync open state
// This allows openFile() calls from anywhere to open the sidebar
const fileEditorStoreOpen = useFileEditorSidebarStore((state) =>
focusedSessionId ? state.sessions[focusedSessionId]?.open : false
);
const fileEditorStoreOpen = useFileEditorSidebarStore((state) => state.open);
useEffect(() => {
if (fileEditorStoreOpen && !fileEditorPanelOpen) {
handleFileEditorPanelOpenChange(true);
Expand Down Expand Up @@ -960,9 +955,8 @@ function App() {
{/* Context Panel - integrated side panel, uses sidecar's current session */}
<ContextPanel open={contextPanelOpen} onOpenChange={handleContextPanelOpenChange} />

{/* File Editor Panel - right side code editor */}
{/* File Editor Panel - right side code editor (shared across all tabs) */}
<FileEditorSidebarPanel
sessionId={focusedSessionId}
open={fileEditorPanelOpen}
onOpenChange={handleFileEditorPanelOpenChange}
workingDirectory={workingDirectory}
Expand Down
2 changes: 1 addition & 1 deletion frontend/components/CommandBlock/StaticTerminalOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export function StaticTerminalOutput({
// Store the detected path for resolution
const pendingDetectedRef = useRef<DetectedPath | null>(null);

const { openFile } = useFileEditorSidebar(sessionId ?? null, workingDirectory);
const { openFile } = useFileEditorSidebar(workingDirectory);

// Calculate rows needed for content (pre-render estimate)
const lineCount = output.split("\n").length;
Expand Down
105 changes: 51 additions & 54 deletions frontend/components/FileEditorSidebar/FileEditorSidebarPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,33 +70,30 @@ function registerVimCommands() {
const args = params.args || [];
const arg = args[0]?.toLowerCase();

// Get current session ID from the store (find the one with vimMode enabled)
const state = useFileEditorSidebarStore.getState();
const sessionId = Object.keys(state.sessions).find((id) => state.sessions[id]?.vimMode);
if (!sessionId) return;

switch (arg) {
case "wrap":
state.setWrap(sessionId, true);
state.setWrap(true);
break;
case "nowrap":
state.setWrap(sessionId, false);
state.setWrap(false);
break;
case "number":
case "nu":
state.setLineNumbers(sessionId, true);
state.setLineNumbers(true);
break;
case "nonumber":
case "nonu":
state.setLineNumbers(sessionId, false);
state.setLineNumbers(false);
break;
case "relativenumber":
case "rnu":
state.setRelativeLineNumbers(sessionId, true);
state.setRelativeLineNumbers(true);
break;
case "norelativenumber":
case "nornu":
state.setRelativeLineNumbers(sessionId, false);
state.setRelativeLineNumbers(false);
break;
}
});
Expand Down Expand Up @@ -155,7 +152,6 @@ function registerVimCommands() {
}

interface FileEditorSidebarPanelProps {
sessionId: string | null;
open: boolean;
onOpenChange: (open: boolean) => void;
workingDirectory?: string | null;
Expand Down Expand Up @@ -284,16 +280,21 @@ function FileOpenPrompt({
}

export function FileEditorSidebarPanel({
sessionId,
open,
onOpenChange,
workingDirectory,
}: FileEditorSidebarPanelProps) {
const {
session,
activeTabId,
activeTab,
activeFile,
tabs,
vimMode,
vimModeState,
wrap,
lineNumbers,
recentFiles,
width,
openFile,
openBrowser,
saveFile,
Expand All @@ -310,7 +311,7 @@ export function FileEditorSidebarPanel({
setVimModeState,
toggleMarkdownPreview,
reorderTabs,
} = useFileEditorSidebar(sessionId, workingDirectory || undefined);
} = useFileEditorSidebar(workingDirectory || undefined);

const [containerWidth, setContainerWidth] = useState(DEFAULT_WIDTH);
const isResizing = useRef(false);
Expand All @@ -319,40 +320,40 @@ export function FileEditorSidebarPanel({

// Navigate to next/previous tab
const goToNextTab = useCallback(() => {
if (!session || tabs.length <= 1) return;
const currentIndex = session.activeTabId ? session.tabOrder.indexOf(session.activeTabId) : -1;
if (tabs.length <= 1) return;
const tabOrder = tabs.map((tab) => tab.id);
const currentIndex = activeTabId ? tabOrder.indexOf(activeTabId) : -1;
const nextIndex = (currentIndex + 1) % tabs.length;
const nextId = session.tabOrder[nextIndex];
const nextId = tabOrder[nextIndex];
if (nextId) setActiveTab(nextId);
}, [session, tabs.length, setActiveTab]);
}, [activeTabId, tabs, setActiveTab]);

const goToPrevTab = useCallback(() => {
if (!session || tabs.length <= 1) return;
const currentIndex = session.activeTabId ? session.tabOrder.indexOf(session.activeTabId) : -1;
if (tabs.length <= 1) return;
const tabOrder = tabs.map((tab) => tab.id);
const currentIndex = activeTabId ? tabOrder.indexOf(activeTabId) : -1;
const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
const prevId = session.tabOrder[prevIndex];
const prevId = tabOrder[prevIndex];
if (prevId) setActiveTab(prevId);
}, [session, tabs.length, setActiveTab]);
}, [activeTabId, tabs, setActiveTab]);

useEffect(() => {
if (session?.width) {
setContainerWidth(session.width);
if (width) {
setContainerWidth(width);
}
}, [session?.width]);
}, [width]);

useEffect(() => {
if (sessionId) {
setOpen(open);
}
}, [open, setOpen, sessionId]);
setOpen(open);
}, [open, setOpen]);

useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing.current) return;
const newWidth = window.innerWidth - e.clientX;
if (newWidth >= MIN_WIDTH && newWidth <= MAX_WIDTH) {
setContainerWidth(newWidth);
if (sessionId) setWidth(newWidth);
setWidth(newWidth);
}
};

Expand All @@ -371,7 +372,7 @@ export function FileEditorSidebarPanel({
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [setWidth, sessionId]);
}, [setWidth]);

const onStartResize = useCallback((e: React.MouseEvent) => {
e.preventDefault();
Expand All @@ -382,7 +383,7 @@ export function FileEditorSidebarPanel({

// Register custom vim commands when vim mode is first enabled
useEffect(() => {
if (session?.vimMode) {
if (vimMode) {
registerVimCommands();
setVimCallbacks({
save: () => void saveFile(),
Expand All @@ -391,16 +392,14 @@ export function FileEditorSidebarPanel({
closeTab();
// Check if we still have tabs after closing
const state = useFileEditorSidebarStore.getState();
const currentSession = sessionId ? state.sessions[sessionId] : null;
if (!currentSession || currentSession.tabOrder.length === 0) {
if (state.tabOrder.length === 0) {
onOpenChange(false);
}
},
forceClose: () => {
closeTab();
const state = useFileEditorSidebarStore.getState();
const currentSession = sessionId ? state.sessions[sessionId] : null;
if (!currentSession || currentSession.tabOrder.length === 0) {
if (state.tabOrder.length === 0) {
onOpenChange(false);
}
},
Expand Down Expand Up @@ -435,19 +434,18 @@ export function FileEditorSidebarPanel({
});
};
}, [
session?.vimMode,
vimMode,
saveFile,
reloadFile,
closeTab,
closeAllTabs,
onOpenChange,
sessionId,
goToNextTab,
goToPrevTab,
]);

useEffect(() => {
if (!session?.vimMode || !editorRef.current?.view) return;
if (!vimMode || !editorRef.current?.view) return;

// biome-ignore lint/suspicious/noExplicitAny: CodeMirror vim internals not fully typed
const cm = (editorRef.current.view as any).cm;
Expand All @@ -465,17 +463,17 @@ export function FileEditorSidebarPanel({
return () => {
cm.off("vim-mode-change", handler);
};
}, [session?.vimMode, setVimModeState]);
}, [vimMode, setVimModeState]);

const extensions = useMemo(() => {
const ext: Extension[] = [];

const lang = languageExtension(activeFile?.language);
if (lang) ext.push(lang);
if (session?.wrap) {
if (wrap) {
ext.push(EditorView.lineWrapping);
}
if (session?.vimMode) {
if (vimMode) {
ext.push(vim());
}
// Keymap for save shortcut inside the editor
Expand All @@ -493,19 +491,19 @@ export function FileEditorSidebarPanel({
);

return ext;
}, [saveFile, activeFile?.language, session?.vimMode, session?.wrap]);
}, [saveFile, activeFile?.language, vimMode, wrap]);

// Memoize basicSetup to react to line number settings changes
const basicSetup = useMemo(
() => ({
lineNumbers: session?.lineNumbers ?? true,
lineNumbers: lineNumbers ?? true,
foldGutter: true,
highlightActiveLine: true,
}),
[session?.lineNumbers]
[lineNumbers]
);

if (!open || !sessionId) return null;
if (!open) return null;

const hasTabs = tabs.length > 0;

Expand All @@ -517,7 +515,7 @@ export function FileEditorSidebarPanel({
workingDirectory={workingDirectory ?? undefined}
onOpen={(path) => openFile(path)}
onOpenBrowser={() => openBrowser()}
recentFiles={session?.recentFiles ?? []}
recentFiles={recentFiles}
/>
);
}
Expand Down Expand Up @@ -651,14 +649,13 @@ export function FileEditorSidebarPanel({
{hasTabs && (
<TabBar
tabs={tabs}
activeTabId={session?.activeTabId ?? null}
activeTabId={activeTabId}
onSelectTab={setActiveTab}
onCloseTab={(tabId) => {
closeTab(tabId);
// If no tabs left, close panel
const state = useFileEditorSidebarStore.getState();
const currentSession = sessionId ? state.sessions[sessionId] : null;
if (!currentSession || currentSession.tabOrder.length === 0) {
if (state.tabOrder.length === 0) {
onOpenChange(false);
}
}}
Expand All @@ -677,9 +674,9 @@ export function FileEditorSidebarPanel({
{/* Footer */}
<div className="px-3 py-2 border-t border-border text-xs text-muted-foreground flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0">
{session?.vimMode && activeTab?.type === "file" && (
{vimMode && activeTab?.type === "file" && (
<Badge variant="outline" className="text-[11px] font-mono uppercase">
{session?.vimModeState ?? "normal"}
{vimModeState ?? "normal"}
</Badge>
)}
{activeTab?.type === "browser" && (
Expand All @@ -692,14 +689,14 @@ export function FileEditorSidebarPanel({
{activeTab?.type === "file" && (
<button
type="button"
onClick={() => setVimMode(!session?.vimMode)}
onClick={() => setVimMode(!vimMode)}
className={cn(
"text-[11px] px-1.5 py-0.5 rounded transition-colors",
session?.vimMode
vimMode
? "bg-primary/20 text-primary hover:bg-primary/30"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
)}
title={session?.vimMode ? "Disable Vim mode" : "Enable Vim mode"}
title={vimMode ? "Disable Vim mode" : "Enable Vim mode"}
>
Vim
</button>
Expand Down
5 changes: 1 addition & 4 deletions frontend/components/FilePathLink/FilePathLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ interface FilePathLinkProps {
detected: DetectedPath;
/** Working directory for path resolution */
workingDirectory: string;
/** Session ID for file editor */
sessionId: string;
/** The text to display (may differ from detected.raw if we only wrap part of it) */
children: ReactNode;
/** Pre-resolved absolute path (if known from index) */
Expand All @@ -27,15 +25,14 @@ interface FilePathLinkProps {
export function FilePathLink({
detected,
workingDirectory,
sessionId,
children,
absolutePath,
}: FilePathLinkProps) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [resolvedPaths, setResolvedPaths] = useState<ResolvedPath[]>([]);

const { openFile } = useFileEditorSidebar(sessionId, workingDirectory);
const { openFile } = useFileEditorSidebar(workingDirectory);

const handleClick = useCallback(async () => {
if (open) {
Expand Down
1 change: 0 additions & 1 deletion frontend/components/Markdown/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ function processTextWithFilePaths(text: string, context: MarkdownContextValue):
<FilePathLink
key={`path-${idx}`}
detected={detected}
sessionId={sessionId}
workingDirectory={workingDirectory}
absolutePath={detected.absolutePath}
>
Expand Down
Loading
Loading