command hq: web frontend v1 — nav, grid, detail

Builds the Workspace/Panel screen the design doc's "Layout, v1" described but
nothing had implemented yet: /command-hq and /command-hq/:sessionId, three
always-mounted panels (officerdev/src/apps/CommandHQ/) — a flat category nav
filtering the grid via ?category=, a commander grid reading GET /commanders,
and a detail pane that is either an empty state, the new-commander composer, or
the selected commander's resumed transcript.

Follows the frontend route conventions in CLAUDE.md throughout: Workspace/Panel
framework (locked, three fixed panels, no standalone screen), category and
session both addressable in the URL rather than a channel, real <Link>s for
every row and card, a route pair (/command-hq + /command-hq/:sessionId) the
same shape as /chat's.

The composer reuses the exact mechanism /chat already uses to create a
commander — an ordinary turn with context: 'command-hq' via useChat/
EmbeddableChat — rather than the mobile-only POST /commanders/new HTTP route.
Once the harness reports a real transcript id the URL is replaced onto it
(useChat's own permalink writes /chat/<id>, which is the wrong screen here, so
that's done by hand). fileAsCommander always files a new commander
uncategorised — it has no way to know a category was selected in the composer
— so if one was, a client-side PATCH sets it after the fact, the same race
POST /commanders/new already resolves server-side for mobile, just resolved
here on the client instead.

The detail panel's transcript resume mirrors ChatPanelWrapper's fetch-then-
mount pattern (GET /chat/sessions/:id first, then useChat with
resumeSessionId + initialMessages + paginate) rather than reusing it directly
— Command HQ's version is simpler (no dashboard/agent-panel concerns) and
scopes rename/category/disarm through Command HQ's own PATCH route rather
than /chat's, since a commander lives in a different directory than an
ordinary chat session.

Card status is the three-state read COMMS/command-hq/mobile-4.md proposed and
the design record adopted: green running, amber armed-but-not-running (the
watcherArmed && !running pair that makes a silently-dead watcher visible),
grey neither.

Registered in AppRegistry.tsx, re-exported from officerdev's barrel (route
helpers + types, per CLAUDE.md's rule for anything an officer-web screen
needs), a CORE_DOCK_ITEMS tile (not in DEFAULT_DOCK_PATHS — opt-in via the
Dock settings screen, same as Jobs/Monitor/Activity), and a usePageTitle rule.

bunx tsgo clean, suite at its 7 pre-existing baseline failures, none of them
this feature's. NOT yet clicked through against a running server — the
composer's redirect-on-real-id, the disarm button, the category-race PATCH
have all typechecked and nothing more. That's the next real test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 09:41:13 +00:00
co-authored by Claude Opus 5
parent 7b7d8bbf36
commit a9be3a92c9
18 changed files with 796 additions and 10 deletions
@@ -0,0 +1,25 @@
import type { LayoutNode } from 'officerdev';
import { WorkspaceView } from 'officerdev';
import { useDashboardState } from 'state/useDashboardState';
import { defaultLayout } from './defaultLayout';
// /command-hq: nav + grid + detail, all three 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" and "Layout, v1".
export const CommandHqScreen = () => {
const workspace = useDashboardState<LayoutNode>('screens/command-hq', defaultLayout);
return (
<div className="h-full w-full pt-2">
<WorkspaceView
workspace={workspace}
locked
appTypes={{
allowed: ['command-hq-categories', 'command-hq-grid', 'command-hq-detail'],
fallback: 'command-hq-detail',
}}
/>
</div>
);
};
@@ -0,0 +1,14 @@
import type { LayoutNode } from 'officerdev';
// Three columns, per `docs/command-hq.md` → "Layout, v1": category nav, commander grid, detail pane. The
// detail pane gets the most room because it is a live chat transcript once something is selected.
export const defaultLayout: LayoutNode = {
type: 'group',
id: 'command-hq-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'command-hq-categories', appType: 'command-hq-categories' }, size: 16 },
{ node: { type: 'panel', id: 'command-hq-grid', appType: 'command-hq-grid' }, size: 34 },
{ node: { type: 'panel', id: 'command-hq-detail', appType: 'command-hq-detail' }, size: 50 },
],
};
@@ -0,0 +1 @@
export * from './CommandHqScreen';