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 (