a commander writes into its own directory — procedures, reports, the loop it armed — and there was no way to look at any of it. /files cannot reach it and never could: the file browser serves two roots, `home` and `user-data`, and a commander lives at $DATA_PATH/<email>/commanders/<dir>, which is under the one /files does not use. so the Plan button becomes a files button on a commander. instead, not beside: plan mode is research-and-propose-until-a-human-approves, and a commander is the case where nobody is watching to approve. the control promised something that cannot happen there, while the thing genuinely missing was any view of what it had written. an EPHEMERAL panel, the same trade /files makes for the file viewer. /command-hq is a locked workspace with a three-item allow-list whose comments are explicit that its structure is code's rather than the user's; a persisted panel would mean unlocking it and widening that list. an ephemeral layout renders beside workspace.value and never enters it, so neither has to change. ?files=1 rather than a route, and a panel rather than a link: the transcript stays mounted and stays the page, and a link on this surface opens a second copy of the whole app in a new tab and loses the conversation you were reading. breadcrumbs stop at the commander for free — Breadcrumb renders relative to basePath and its home crumb IS the base. worth knowing that this is the UI's floor and not the server's: the root really is all of user-data, which is the owner's own data either way, so it bounds the navigation rather than the account. the button reads its state from props rather than useSearchParams. ModelSelector is rendered bare in its own tests and reaching for the router inside it failed all twelve — a control that has to be mounted in a route to render is not a control any more. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
45 lines
1.9 KiB
TypeScript
45 lines
1.9 KiB
TypeScript
import { useEffect } from 'react';
|
|
import type { LayoutNode } from 'officerdev';
|
|
import { WorkspaceView, useCommanderFilesPanel } from 'officerdev';
|
|
import { useDashboardState } from 'state/useDashboardState';
|
|
import { defaultLayout, isLegacyThreeColumn } from './defaultLayout';
|
|
|
|
// /command-hq: navigation and detail, all panels always mounted (like /chat), selection carried by the URL
|
|
// — `?category=` for the nav filter, `:sessionId` for the open commander. See `docs/command-hq.md` →
|
|
// "Route, surface, placement".
|
|
|
|
export const CommandHqScreen = () => {
|
|
const workspace = useDashboardState<LayoutNode>('screens/command-hq', defaultLayout);
|
|
const ephemeral = useCommanderFilesPanel();
|
|
|
|
/*
|
|
Move a saved three-column layout onto the two-column one.
|
|
|
|
The default alone cannot do it: this key is persisted per account, so anybody who has opened the screen
|
|
keeps the arrangement they had. See `isLegacyThreeColumn` for why the test is structural rather than an
|
|
appType check, and why replacing outright is the safe direction on a `locked` screen.
|
|
*/
|
|
useEffect(() => {
|
|
if (!workspace.isLoaded) return;
|
|
if (!isLegacyThreeColumn(workspace.value)) return;
|
|
workspace.setValue(defaultLayout);
|
|
}, [workspace.isLoaded, workspace.value, workspace.setValue]);
|
|
|
|
return (
|
|
<div className="h-full w-full pt-2">
|
|
{/* The commander's own file browser is a layout of its own, rendered beside this one — it never
|
|
enters `workspace.value`, so this screen stays locked and the allow-list does not have to know
|
|
about it. Same trade `/files` makes for the file viewer. */}
|
|
<WorkspaceView
|
|
workspace={workspace}
|
|
locked
|
|
appTypes={{
|
|
allowed: ['command-hq-categories', 'command-hq-grid', 'command-hq-detail'],
|
|
fallback: 'command-hq-detail',
|
|
}}
|
|
ephemeral={ephemeral}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|