diff --git a/docs/navigation-audit.md b/docs/navigation-audit.md index 25421553..e933843a 100644 --- a/docs/navigation-audit.md +++ b/docs/navigation-audit.md @@ -73,7 +73,7 @@ are good building blocks. The **Workspace/Panel framework** contains **zero** ro | ID | file:line | Entity | Proposed route | Note | |----|-----------|--------|----------------|------| -| M1 | `Screens/Dashboard/CapabilityPage.tsx:431` | task / skill / process | `/tasks/:dir`, `/skills/:dir`, `/processes/:dir` | one component backs **three** screens (Tasks/Skills/Processes). Highest-value MEDIUM. Also drops the auto-select-`items[0]` effect. | +| ~~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` | detail fetch already keys off the id — clean move. | | M3 | `Screens/Dashboard/Activity/ActivityScreen.tsx:63,73` | background task / detached job | `/activity/:id` | two row types; unify under one param, screen re-derives `task=`/`path=`. | | 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. | @@ -210,7 +210,7 @@ publishers means changing the chat panel, which is another agent's, so it is wri ### Phase 2 — Add a route, then link (per-entity, medium effort) - [x] **M6** Settings sub-sections → `/settings/:page/:section`; `SectionButton` is now a `SectionLink` (``), the five `*_SELECTED` globals and `INTEGRATIONS_SETTINGS_TAB` are gone, and each page renders one `SettingsRoute` guard that canonicalises the bare route and a bogus section. **Needs runtime test.** -- [ ] **M1** Capabilities → `/tasks|skills|processes/:dir`, rows → `` (`CapabilityPage.tsx:431`) — covers 3 screens. +- [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.** - [ ] **M2** TaskLogs → `/task-logs/:id` (`TaskLogs/index.tsx:104`). - [ ] **M3** Activity → `/activity/:id` (`ActivityScreen.tsx:63,73`). - [ ] **M4** FileBrowser folders → `/files?path=`; folder rows + breadcrumbs → `` (`useFileBrowserApp.ts:269`, `FileItem.tsx`, `Breadcrumb.tsx`). diff --git a/src/apps/officer-web/App.tsx b/src/apps/officer-web/App.tsx index 6a767338..4f83b3c5 100644 --- a/src/apps/officer-web/App.tsx +++ b/src/apps/officer-web/App.tsx @@ -85,8 +85,11 @@ export function App() { } /> } /> + } /> } /> + } /> } /> + } /> } /> } /> } /> diff --git a/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx b/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx index 072967ab..73772dfa 100644 --- a/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx +++ b/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from 'react'; +import { Link, useNavigate, useParams, useSearchParams } from 'react-router'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -27,9 +28,10 @@ type CapabilityListProps = { kind: string; endpoint: string; queryKey: string; + /** Route root for this capability — `/tasks`, `/skills`, `/processes`. Rows link to `/`. */ + basePath: string; selected: string | null; - onSelect: (dirName: string) => void; - onCreate?: (dirName: string) => void; + onCreate: (dirName: string) => void; search?: string; showCreate?: boolean; onShowCreateChange?: (value: boolean) => void; @@ -39,6 +41,8 @@ type CapabilityPageProps = { kind: string; endpoint: string; queryKey: string; + /** Route root — given explicitly rather than reused from `endpoint`, which only happens to match. */ + basePath: string; }; type CapabilityChatProps = { @@ -318,8 +322,8 @@ export const CapabilityList = ({ kind, endpoint, queryKey, + basePath, selected, - onSelect, onCreate, search: externalSearch, showCreate, @@ -348,7 +352,7 @@ export const CapabilityList = ({ await qc.invalidateQueries({ queryKey: [queryKey] }); setCreating(false); setNewName(''); - (onCreate ?? onSelect)(res.dirName); + onCreate(res.dirName); } catch { toast.error(`Failed to create ${kind}`); } @@ -429,10 +433,10 @@ export const CapabilityList = ({ )}
{filtered.map((item) => ( -
{item.description &&

{item.description}

} - + ))} {items.length === 0 && (

No {kind.toLowerCase()}s found

@@ -453,25 +457,26 @@ export const CapabilityList = ({ ); }; -export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps) => { +/** + * Backs /tasks, /skills and /processes. Which capability is open is `/:dirName` — a route pair + * with no redirect guard, because the bare route (the list with nothing open) is a real state and an + * unknown dirName gets the empty detail pane rather than a rewritten address. + * + * The two per-item modes ride along as search params instead of local state: a row is a now, so it + * cannot imperatively reset the panes it changes, and deriving them means navigating to another item drops + * them for free — no reset effect racing the navigation. `edit` also makes the editor pane linkable. + */ +export const CapabilityPage = ({ kind, endpoint, queryKey, basePath }: CapabilityPageProps) => { const client = useClient(); const qc = useQueryClient(); - const [selected, setSelected] = useState(null); - const [editing, setEditing] = useState(false); - const [isNew, setIsNew] = useState(false); + const navigate = useNavigate(); + const selected = useParams<{ dirName: string }>().dirName ?? null; + const [params, setParams] = useSearchParams(); + const editing = params.get('edit') === '1'; + const isNew = params.get('new') === '1'; const [deleteConfirm, setDeleteConfirm] = useState(false); - const [showDetail, setShowDetail] = useState(false); - const { data: items = [] } = useQuery({ - queryKey: [queryKey], - queryFn: () => client.get(endpoint), - }); - - useEffect(() => { - if (items.length > 0 && !selected) { - setSelected(items[0]!.dirName); - } - }, [items, selected]); + const setEditing = (on: boolean) => setParams(on ? { edit: '1' } : {}, { replace: true }); const { data: detail } = useQuery({ queryKey: [queryKey, selected], @@ -479,28 +484,15 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps enabled: !!selected, }); - const selectItem = (dirName: string) => { - setSelected(dirName); - setShowDetail(true); - setIsNew(false); - setEditing(false); - }; - - const handleCreate = (dirName: string) => { - setSelected(dirName); - setShowDetail(true); - setIsNew(true); - setEditing(true); - }; + // A brand-new capability is empty, so it opens straight into the chat that fills it in. + const handleCreate = (dirName: string) => navigate(`${basePath}/${encodeURIComponent(dirName)}?edit=1&new=1`); const handleDelete = async () => { if (!selected) return; try { await client.delete(`${endpoint}/${selected}`); setDeleteConfirm(false); - setEditing(false); - setSelected(null); - setShowDetail(false); + navigate(basePath, { replace: true }); await qc.invalidateQueries({ queryKey: [queryKey] }); } catch { toast.error(`Failed to delete ${kind}`); @@ -512,35 +504,32 @@ export const CapabilityPage = ({ kind, endpoint, queryKey }: CapabilityPageProps
{/* Left panel — list */} - {/* Right panel — detail + chat */} -
+ {/* Right panel — detail + chat. On mobile the two panes swap on `selected`; going back is the bare route. */} +
- + {detail?.name ?? `Select a ${kind.toLowerCase()}`} {detail && ( <>