diff --git a/docs/navigation-audit.md b/docs/navigation-audit.md
index c79df7db..f51050e3 100644
--- a/docs/navigation-audit.md
+++ b/docs/navigation-audit.md
@@ -42,9 +42,9 @@ was in this list; the Projects feature was removed end to end on 2026-07-30, so
and navigate-site 22 below still name it — they are the record of work that happened, not of code that exists.)
**Section routes** (a param that names a view rather than an entity): `/settings/:page/:section`,
`/soulseek/:section`, `/headscale/:section`, `/wallet/:section`, `/system-monitor/:scope`.
-**Query-param screens:** `/music?path=`.
-**Genuinely flat, still:** `/` `/files` `/plans` `/terminal` `/desktop` `/code-editor` `/qr-transfer`.
-`/files` and `/code-editor` are M4 and M5 — the last two that ought not to be.
+**Query-param screens:** `/music?path=`, `/files?path=`.
+**Genuinely flat, still:** `/` `/plans` `/terminal` `/desktop` `/code-editor` `/qr-transfer`.
+`/code-editor` is M5 — the last one that ought not to be.
The five settings pages are route **pairs** now, not flat screens — `/settings/{profile,ai,system,integrations,user-management}`
plus a `:section` each (M6). There has never been a `/settings/apps`; that entry was wrong when this list
was written.
@@ -80,7 +80,7 @@ are good building blocks. The **Workspace/Panel framework** contains **zero** ro
| ~~M1~~ | ~~`Screens/Dashboard/CapabilityPage.tsx:431`~~ | task / skill / process | `/tasks/:dirName`, `/skills/:dirName`, `/processes/:dirName` | **Done.** One component backed three screens, so one change covered all of them. The auto-select-`items[0]` effect is gone — the bare route is now the list with an empty detail pane. `editing`/`isNew` moved to `?edit=1` / `?new=1` because a `` row cannot imperatively reset them. |
| ~~M2~~ | ~~`Screens/Dashboard/TaskLogs/index.tsx:104`~~ | a task-log run | `/task-logs/:id` | **Done.** As predicted — the detail fetch already keyed off the id, so only its source changed. `showDetail` is gone; the mobile swap and both back arrows derive from the param. |
| ~~M3~~ | ~~`Screens/Dashboard/Activity/ActivityScreen.tsx:63,73`~~ | background task / detached job | `/activity/:id` | **Done.** One param for both row kinds; the screen looks the id up in the polled registry and derives `task=`/`path=` from the row. The SSE effect now depends on that derived *string*, so the 3s poll no longer risks re-opening the stream. An id that has left the registry says so instead of hanging on "waiting for output". |
-| M4 | `FileBrowser/.../useFileBrowserApp.ts:269`, `FileItem.tsx:516`, `Breadcrumb.tsx:16`, search-hit `:249` | a folder | `/files?path=
` | **files** already open via `?view=`; **folders** are pure `currentPath` state — no URL, no back/forward. Folder rows + crumbs → `` on a `?path=` param. |
+| ~~M4~~ | ~~`FileBrowser/.../useFileBrowserApp.ts:269`~~, `FileItem.tsx:516`, ~~`Breadcrumb.tsx:16`~~ | a folder | `/files?path=` | **Partly done — the rest is an owner decision, not a defect.** `currentPath` is `?path=` on `/files`, so back/forward and linking a folder work, and the crumbs are ``s. Two things the audit line did not know: `?view=` is *ephemeral* (wiped on mount by `useFileViewerPanels`), so `path` is the screen's first durable param, and four `setSearchParams({…})` calls replaced the whole query string — opening any file would have silently reset the folder. They go through a `setViewerParams` helper now that keeps `path`. Opt-in via the parsed `WorkspaceIdentity` (`screens/files`), because a dashboard can hold two browsers and one shared param would move both. **Folder *items* stay buttons:** ⌘/Ctrl/Shift-click is already bound to multi-select in `FileItem.tsx` and open is double-click, so anchor semantics collide with an existing gesture. |
| M5 | `CodeEditor/FileTree.tsx:59`, `EditorTabs.tsx:33` | open source file / active tab | `/code-editor?file=` (+ `open=` for tabs) | tree file rows → ``; active file in a param. |
| ~~M6~~ | `Settings/SettingsPanel.tsx` | a settings sub-section | `/settings/:page/:section` | **Done.** `` + `useParams`, five `*_SELECTED` globals gone, one `SettingsRoute` guard per page. The "one change covers all settings pages" claim was *almost* right: Integrations builds its own sidebar and did not go through `createSettingsPanelComponents`, and it also held the Enterprise/Personal tab in a second global — derived from the section key now, which is what fixes deep-linking a Personal section. |
| M7 | `workspaces/components/Combobox.tsx:53` | caller-supplied route | (existing) | shared widget: `href`-bearing options do `navigate(option.href)`; render them as ``. Every caller inherits the opaque click. |
@@ -217,7 +217,7 @@ publishers means changing the chat panel, which is another agent's, so it is wri
- [x] **M1** Capabilities → `/tasks|skills|processes/:dirName`, rows → ``; `CapabilityPage` takes an explicit `basePath` (not reused from `endpoint`, which only happens to match). Selection is `useParams`, the mobile pane swap and back arrow are derived from it, delete navigates to the bare route, and the two per-item modes are `?edit=1` / `?new=1`. No `` guard: an unknown `dirName` gets the empty detail pane. **Needs runtime test.**
- [x] **M2** TaskLogs → `/task-logs/:id`; rows are ``s, `showDetail` deleted. **Needs runtime test.**
- [x] **M3** Activity → `/activity/:id`; the `{label, query}` selection object is gone — the id is the URL and the stream query is derived from the registry row. `/activity` also had no `usePageTitle` rule (it read "Officer"); added. **Needs runtime test.**
-- [ ] **M4** FileBrowser folders → `/files?path=`; folder rows + breadcrumbs → `` (`useFileBrowserApp.ts:269`, `FileItem.tsx`, `Breadcrumb.tsx`).
+- [x] **M4** FileBrowser folders → `/files?path=`; breadcrumbs are ``s. Folder *rows* deliberately still buttons — ⌘-click is multi-select, open is double-click; converting them needs an owner call on the gesture.
- [ ] **M5** CodeEditor active file → `/code-editor?file=` (`FileTree.tsx:59`, `EditorTabs.tsx:33`).
- [ ] **M7** Combobox: render `href` options as `` (`Combobox.tsx:53`).
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserApp.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserApp.tsx
index e5de9918..580f091a 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserApp.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserApp.tsx
@@ -11,16 +11,23 @@ import { useFileBrowserApp } from './useFileBrowserApp';
type FileBrowserAppProps = {
basePath?: string;
rootOverride?: string;
+ /** Put the current folder in `?path=`. Opt-in — only the /files screen is guaranteed one browser. */
+ urlPath?: boolean;
};
-export const FileBrowserApp = ({ basePath = '/', rootOverride }: FileBrowserAppProps) => {
- const fileBrowserManager = useFileBrowserApp(basePath, rootOverride);
+export const FileBrowserApp = ({ basePath = '/', rootOverride, urlPath }: FileBrowserAppProps) => {
+ const fileBrowserManager = useFileBrowserApp(basePath, rootOverride, urlPath);
const { handleNavigate } = fileBrowserManager;
return (
-
+
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx
index bc95236c..56a3da55 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/FileBrowserPanelWrapper.tsx
@@ -4,7 +4,14 @@ import { FileBrowserApp } from './FileBrowserApp';
const cwdToPath = (cwd: string) => (cwd === '~' ? '/' : cwd.slice(1));
export const FileBrowserPanelWrapper = () => {
- const { cwd } = useWorkspace();
+ const { cwd, workspace } = useWorkspace();
const basePath = cwdToPath(cwd);
- return ;
+
+ // Only the /files screen puts its folder in the URL. A dashboard can hold two file browsers, and one
+ // shared `?path=` would move both — so the address bar belongs to the workspace that is guaranteed to
+ // host exactly one. The test is the framework's parsed identity, not the base path: an unscoped panel
+ // (cwd `~`) sits on dashboards too, so `basePath === '/'` would catch the wrong browsers.
+ const urlPath = workspace?.kind === 'screen' && workspace.id === 'files';
+
+ return ;
};
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Breadcrumb.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Breadcrumb.tsx
index 1cb0724d..39779c3a 100644
--- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Breadcrumb.tsx
+++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Breadcrumb.tsx
@@ -1,25 +1,43 @@
+import { Link } from 'react-router';
import { ChevronRight, Home } from 'lucide-react';
type BreadcrumbProps = {
path: string;
onNavigate: (path: string) => void;
basePath?: string;
+ /**
+ * Query string for a folder, when this browser owns the address bar. Given, the crumbs are real links
+ * and cmd-click opens the folder in a new tab; absent (a dashboard panel, the widget) they stay buttons,
+ * because that browser's location is not in the URL and there would be nothing for the new tab to read.
+ */
+ searchForPath?: ((path: string) => string) | null;
};
-export const Breadcrumb = ({ path, onNavigate, basePath = '/' }: BreadcrumbProps) => {
+export const Breadcrumb = ({ path, onNavigate, basePath = '/', searchForPath }: BreadcrumbProps) => {
const relativePath = basePath !== '/' && path.startsWith(basePath) ? path.slice(basePath.length) : path;
const segments = relativePath.split('/').filter(Boolean);
+ const Crumb = ({ to, children, className }: { to: string; children: React.ReactNode; className: string }) =>
+ searchForPath ? (
+
+ {children}
+
+ ) : (
+
+ );
+
return (