diff --git a/plugins/file-manager/src/frontend/FileManagerContextMenu.tsx b/plugins/file-manager/src/frontend/FileManagerContextMenu.tsx
index f4301261a..e58f52dc3 100644
--- a/plugins/file-manager/src/frontend/FileManagerContextMenu.tsx
+++ b/plugins/file-manager/src/frontend/FileManagerContextMenu.tsx
@@ -378,7 +378,7 @@ export function FileManagerContextMenu({
icon: ,
label: t("fileManager.rename"),
action: () => onRename(files[0]),
- shortcut: "F6",
+ shortcut: "F2",
});
}
diff --git a/plugins/file-manager/src/frontend/FileManagerGrid.tsx b/plugins/file-manager/src/frontend/FileManagerGrid.tsx
index 6d63f83d7..9e3ad5355 100644
--- a/plugins/file-manager/src/frontend/FileManagerGrid.tsx
+++ b/plugins/file-manager/src/frontend/FileManagerGrid.tsx
@@ -1032,6 +1032,7 @@ export function FileManagerGrid({
onDelete(selectedFiles);
}
break;
+ case "F2":
case "F6":
if (selectedFiles.length === 1 && onStartEdit) {
event.preventDefault();
@@ -1418,6 +1419,7 @@ export function FileManagerGrid({
const isSelected = selectedFiles.some(
(f) => f.path === file.path,
);
+ const isEditing = editingFile?.path === file.path;
return (
{getFileIcon(file, viewMode, compact)}
- {editingFile?.path === file.path ? (
+ {isEditing ? (
setEditingName(e.target.value)}
onKeyDown={handleEditKeyDown}
onBlur={handleEditConfirm}
- className="flex-1 min-w-0 max-w-[200px] border border-accent-brand/60 bg-card px-2 py-1 text-xs rounded-none outline-none focus:ring-1 focus:ring-accent-brand/50 pointer-events-auto"
+ className={cn(
+ "flex-1 min-w-0 border border-accent-brand/60 bg-card px-2 py-0 rounded-none outline-none focus:ring-1 focus:ring-accent-brand/50 pointer-events-auto",
+ compact ? "h-5 text-[11px]" : "h-6 text-xs",
+ )}
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
/>
@@ -1493,13 +1501,13 @@ export function FileManagerGrid({
)}
- {listColumns.isVisible("modified") && (
+ {!isEditing && listColumns.isVisible("modified") && (
{file.modified || "—"}
)}
- {listColumns.isVisible("owner") && (
+ {!isEditing && listColumns.isVisible("owner") && (
{file.owner
? `${file.owner}${file.group ? `:${file.group}` : ""}`
@@ -1507,7 +1515,7 @@ export function FileManagerGrid({
)}
- {listColumns.isVisible("size") && (
+ {!isEditing && listColumns.isVisible("size") && (
{file.type === "file" &&
file.size !== undefined &&
@@ -1517,7 +1525,7 @@ export function FileManagerGrid({
)}
- {listColumns.isVisible("permissions") && (
+ {!isEditing && listColumns.isVisible("permissions") && (
{file.permissions || "—"}
diff --git a/plugins/file-manager/tests/frontend/FileManagerGrid.test.tsx b/plugins/file-manager/tests/frontend/FileManagerGrid.test.tsx
new file mode 100644
index 000000000..fafd1636b
--- /dev/null
+++ b/plugins/file-manager/tests/frontend/FileManagerGrid.test.tsx
@@ -0,0 +1,91 @@
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import "@testing-library/jest-dom/vitest";
+import { FileManagerGrid } from "../../src/frontend/FileManagerGrid";
+import type { FileItem } from "../../src/frontend/host-types";
+
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
+ initReactI18next: { type: "3rdParty", init: () => {} },
+}));
+
+vi.mock("@tanstack/react-virtual", () => ({
+ useVirtualizer: ({ count }: { count: number }) => ({
+ getTotalSize: () => count * 29,
+ getVirtualItems: () =>
+ Array.from({ length: count }, (_, index) => ({
+ index,
+ key: index,
+ start: index * 29,
+ size: 29,
+ })),
+ measureElement: () => {},
+ measure: () => {},
+ }),
+}));
+
+const file: FileItem = {
+ name: "notes.txt",
+ path: "/notes.txt",
+ type: "file",
+ modified: "2026-09-01",
+ owner: "owner",
+ permissions: "-rw-r--r--",
+};
+const props = {
+ files: [file],
+ selectedFiles: [file],
+ viewMode: "list" as const,
+ onFileOpen: vi.fn(),
+ onSelectionChange: vi.fn(),
+ onRefresh: vi.fn(),
+};
+
+afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+});
+
+describe("remote file rename", () => {
+ it.each(["F2", "F6"])("starts rename with %s in the visible pane", (key) => {
+ vi.spyOn(HTMLElement.prototype, "offsetParent", "get").mockReturnValue(
+ document.body,
+ );
+ const onStartEdit = vi.fn();
+ render();
+ fireEvent.keyDown(document, { key });
+ expect(onStartEdit).toHaveBeenCalledExactlyOnceWith(file);
+ });
+
+ it("does not rename files in a hidden pane", () => {
+ vi.spyOn(HTMLElement.prototype, "offsetParent", "get").mockReturnValue(
+ null,
+ );
+ const onStartEdit = vi.fn();
+ render();
+ fireEvent.keyDown(document, { key: "F2" });
+ expect(onStartEdit).not.toHaveBeenCalled();
+ });
+
+ it("hides metadata while editing and commits the new name", () => {
+ const onRename = vi.fn();
+ const { rerender } = render();
+ expect(screen.getByText(file.modified!)).toBeInTheDocument();
+ rerender(
+ ,
+ );
+ expect(screen.queryByText(file.modified!)).not.toBeInTheDocument();
+ expect(screen.queryByText(file.permissions!)).not.toBeInTheDocument();
+ const input = screen.getByRole("textbox");
+ fireEvent.change(input, { target: { value: "renamed.txt" } });
+ fireEvent.keyDown(input, { key: "Enter" });
+ expect(onRename).toHaveBeenCalledWith(file, "renamed.txt");
+ rerender();
+ expect(screen.getByText(file.modified!)).toBeInTheDocument();
+ });
+});