diff --git a/frontend/editor/src/core/components/filesPage/useFileLibraryWorkbenchBarButtons.tsx b/frontend/editor/src/core/components/filesPage/useFileLibraryWorkbenchBarButtons.tsx
index f33e3f5093..68670840e1 100644
--- a/frontend/editor/src/core/components/filesPage/useFileLibraryWorkbenchBarButtons.tsx
+++ b/frontend/editor/src/core/components/filesPage/useFileLibraryWorkbenchBarButtons.tsx
@@ -45,7 +45,7 @@ export function useFileLibraryWorkbenchBarButtons({
refreshDisabledReason ??
t("filesPage.refresh", "Refresh from server"),
ariaLabel: t("filesPage.refresh", "Refresh from server"),
- section: "top",
+ section: "bar",
order: 10,
disabled: refreshing || Boolean(refreshDisabledReason),
onClick: onRefresh,
@@ -56,7 +56,7 @@ export function useFileLibraryWorkbenchBarButtons({
tooltip:
newFolderDisabledReason ?? t("filesPage.newFolder", "New folder"),
ariaLabel: t("filesPage.newFolder", "New folder"),
- section: "top",
+ section: "bar",
order: 20,
disabled: Boolean(newFolderDisabledReason),
onClick: onNewFolder,
@@ -66,7 +66,7 @@ export function useFileLibraryWorkbenchBarButtons({
icon: ,
tooltip: t("filesPage.upload", "Upload"),
ariaLabel: t("filesPage.upload", "Upload"),
- section: "top",
+ section: "bar",
order: 30,
onClick: onUpload,
},
@@ -78,7 +78,7 @@ export function useFileLibraryWorkbenchBarButtons({
icon: ,
tooltip: t("filesPage.uploadFromMobile", "Upload from Mobile"),
ariaLabel: t("filesPage.uploadFromMobile", "Upload from Mobile"),
- section: "top" as const,
+ section: "bar" as const,
order: 40,
onClick: onUploadFromMobile,
},
diff --git a/frontend/editor/src/core/components/shared/WorkbenchBar.tsx b/frontend/editor/src/core/components/shared/WorkbenchBar.tsx
index 5602dcd183..eed682dccb 100644
--- a/frontend/editor/src/core/components/shared/WorkbenchBar.tsx
+++ b/frontend/editor/src/core/components/shared/WorkbenchBar.tsx
@@ -169,6 +169,14 @@ export default function WorkbenchBar({
return selectedFileIds.length;
}, [currentView, pageEditorSelectedCount, selectedFileIds.length]);
+ // Registered into the bar's own row rather than the tool row below it. Already
+ // sorted by order when registered.
+ const barRowButtons = useMemo(
+ () =>
+ buttons.filter((btn) => btn.section === "bar" && (btn.visible ?? true)),
+ [buttons],
+ );
+
const sectionsWithButtons = useMemo(() => {
return SECTION_ORDER.map((section) => {
const sectionButtons = buttons.filter(
@@ -598,6 +606,19 @@ export default function WorkbenchBar({
{/* Right: Global buttons - export group left, close anchored right */}
+ {/* A view's own controls, ahead of the globals every view shares. */}
+ {barRowButtons.map((btn) => {
+ const content = renderButton(btn);
+ if (!content) return null;
+ return (
+
+ {content}
+
+ );
+ })}
+ {barRowButtons.length > 0 && (
+
+ )}
{/* Share (viewer only; opens the same modal as My Files "Manage sharing") */}
{currentView === "viewer" && sharingEnabled && (
diff --git a/frontend/editor/src/core/pages/HomePage.tsx b/frontend/editor/src/core/pages/HomePage.tsx
index e0096675bd..2e4440d680 100644
--- a/frontend/editor/src/core/pages/HomePage.tsx
+++ b/frontend/editor/src/core/pages/HomePage.tsx
@@ -73,6 +73,15 @@ function readSwipeHintSeen(): boolean {
}
}
+/**
+ * The path this last derived a view from. Module scope on purpose: HomePage remounts
+ * (a share link, a login bounce, a Suspense boundary resolving) and a per-mount ref
+ * would read the unchanged path as a fresh arrival, re-imposing the library over a
+ * view the user had just picked. Reset by a real page load, which is when a path does
+ * need deriving again.
+ */
+let lastSyncedPath: string | null = null;
+
function readPersistedSidebarCollapsed(): boolean {
try {
return (
@@ -231,10 +240,9 @@ export default function HomePage() {
// Path moved, so the path is the cause: arrival, back/forward, or a deliberate
// navigate. Mount included, which is what seeds a deep link.
- const syncedPathRef = useRef
(null);
useEffect(() => {
- if (syncedPathRef.current === location.pathname) return;
- syncedPathRef.current = location.pathname;
+ if (lastSyncedPath === location.pathname) return;
+ lastSyncedPath = location.pathname;
if (location.pathname.startsWith("/files")) {
if (navigationState.workbench !== "myFiles") {
actions.setWorkbench("myFiles");
diff --git a/frontend/editor/src/core/tests/stubbed/file-library-view.spec.ts b/frontend/editor/src/core/tests/stubbed/file-library-view.spec.ts
index 96afdf00f8..e9aabd9b01 100644
--- a/frontend/editor/src/core/tests/stubbed/file-library-view.spec.ts
+++ b/frontend/editor/src/core/tests/stubbed/file-library-view.spec.ts
@@ -20,6 +20,11 @@ test.describe("The file library behaves like the other views", () => {
await railButton(page, /^File library$/i).click();
await expect(page).toHaveURL(/\/files/);
+ // The path leads the view by a render, so wait for the library itself: clicking
+ // the next control before it mounts races its own arrival.
+ await expect(page.getByRole("tree", { name: /Folders/i })).toBeVisible({
+ timeout: 15_000,
+ });
// Reader sets the viewer workbench. The path leaving /files is that view change
// reaching the URL - which is what the old reconciler undid.
diff --git a/frontend/editor/src/core/types/workbenchBar.ts b/frontend/editor/src/core/types/workbenchBar.ts
index 523aaab681..54e2a74b79 100644
--- a/frontend/editor/src/core/types/workbenchBar.ts
+++ b/frontend/editor/src/core/types/workbenchBar.ts
@@ -1,6 +1,13 @@
import React from "react";
-export type WorkbenchBarSection = "top" | "middle" | "bottom" | "tool-panel";
+/** "bar" renders in the bar's own row beside the view switcher; top/middle/bottom
+ * are lanes of the retractable tool row beneath it. */
+export type WorkbenchBarSection =
+ | "bar"
+ | "top"
+ | "middle"
+ | "bottom"
+ | "tool-panel";
export type WorkbenchBarAction = () => void;