feat(swarm): implement Swarm View remake with Operations, Archetypes, and Templates
This commit includes the new SwarmWorkspace with its 3 sub-tabs, the LeftPanel mission picker, and the comprehensive Operations Command Dashboard featuring the live interactive DAG telemetry and task assignment prep flow.
This commit is contained in:
parent
409a7e7256
commit
dfaf523029
74 changed files with 11066 additions and 2046 deletions
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
|
||||
export type ViewType = 'social' | 'graph' | 'swarm' | 'activity';
|
||||
|
|
@ -12,13 +12,22 @@ export interface UrlState {
|
|||
view: ViewType;
|
||||
setView: (v: ViewType) => void;
|
||||
taskId: string | null;
|
||||
setTaskId: (id: string | null) => void;
|
||||
setTaskId: (id: string | null, openDrawer?: boolean) => void;
|
||||
swarmId: string | null;
|
||||
setSwarmId: (id: string | null) => void;
|
||||
setSwarmId: (id: string | null, openDrawer?: boolean) => void;
|
||||
agentId: string | null;
|
||||
setAgentId: (id: string | null) => void;
|
||||
epicId: string | null;
|
||||
setEpicId: (id: string | null) => void;
|
||||
leftPanel: PanelState;
|
||||
setLeftPanel: (state: PanelState) => void;
|
||||
toggleLeftPanel: () => void;
|
||||
rightPanel: PanelState;
|
||||
setRightPanel: (state: PanelState) => void;
|
||||
toggleRightPanel: () => void;
|
||||
blockedOnly: boolean;
|
||||
setBlockedOnly: (enabled: boolean) => void;
|
||||
toggleBlockedOnly: () => void;
|
||||
panel: PanelState;
|
||||
togglePanel: () => void;
|
||||
drawer: DrawerState;
|
||||
|
|
@ -29,7 +38,8 @@ export interface UrlState {
|
|||
}
|
||||
|
||||
const DEFAULT_VIEW: ViewType = 'social';
|
||||
const DEFAULT_PANEL: PanelState = 'open';
|
||||
const DEFAULT_LEFT_PANEL: PanelState = 'open';
|
||||
const DEFAULT_RIGHT_PANEL: PanelState = 'open';
|
||||
const DEFAULT_DRAWER: DrawerState = 'closed';
|
||||
const DEFAULT_GRAPH_TAB: GraphTabType = 'flow';
|
||||
|
||||
|
|
@ -38,12 +48,51 @@ const VALID_PANELS: PanelState[] = ['open', 'closed'];
|
|||
const VALID_DRAWERS: DrawerState[] = ['open', 'closed'];
|
||||
const VALID_GRAPH_TABS: GraphTabType[] = ['flow', 'overview'];
|
||||
|
||||
export function parseUrlState(searchParams: URLSearchParams): {
|
||||
const PANEL_STORAGE_KEYS = {
|
||||
left: 'bb.ui.leftPanel',
|
||||
right: 'bb.ui.rightPanel',
|
||||
} as const;
|
||||
|
||||
interface PanelDefaults {
|
||||
leftPanel: PanelState;
|
||||
rightPanel: PanelState;
|
||||
}
|
||||
|
||||
function parsePanelValue(value: string | null): PanelState | null {
|
||||
if (!value || !VALID_PANELS.includes(value as PanelState)) {
|
||||
return null;
|
||||
}
|
||||
return value as PanelState;
|
||||
}
|
||||
|
||||
function readStoredPanelState(key: string, fallback: PanelState): PanelState {
|
||||
if (typeof window === 'undefined') {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const value = window.localStorage.getItem(key);
|
||||
return parsePanelValue(value) ?? fallback;
|
||||
}
|
||||
|
||||
function isBlockedEnabled(value: string | null): boolean {
|
||||
return value === '1' || value === 'true';
|
||||
}
|
||||
|
||||
export function parseUrlState(
|
||||
searchParams: URLSearchParams,
|
||||
defaults: PanelDefaults = {
|
||||
leftPanel: DEFAULT_LEFT_PANEL,
|
||||
rightPanel: DEFAULT_RIGHT_PANEL,
|
||||
}
|
||||
): {
|
||||
view: ViewType;
|
||||
taskId: string | null;
|
||||
swarmId: string | null;
|
||||
agentId: string | null;
|
||||
epicId: string | null;
|
||||
leftPanel: PanelState;
|
||||
rightPanel: PanelState;
|
||||
blockedOnly: boolean;
|
||||
panel: PanelState;
|
||||
drawer: DrawerState;
|
||||
graphTab: GraphTabType;
|
||||
|
|
@ -58,10 +107,15 @@ export function parseUrlState(searchParams: URLSearchParams): {
|
|||
const agentId = searchParams.get('agent');
|
||||
const epicId = searchParams.get('epic');
|
||||
|
||||
const panelParam = searchParams.get('panel');
|
||||
const panel: PanelState = panelParam && VALID_PANELS.includes(panelParam as PanelState)
|
||||
? (panelParam as PanelState)
|
||||
: DEFAULT_PANEL;
|
||||
const leftPanelFromUrl = parsePanelValue(searchParams.get('left'));
|
||||
const rightPanelFromUrl = parsePanelValue(searchParams.get('right'));
|
||||
const legacyPanel = parsePanelValue(searchParams.get('panel'));
|
||||
|
||||
const leftPanel = leftPanelFromUrl ?? defaults.leftPanel;
|
||||
const rightPanel = rightPanelFromUrl ?? legacyPanel ?? defaults.rightPanel;
|
||||
const panel = rightPanel;
|
||||
|
||||
const blockedOnly = isBlockedEnabled(searchParams.get('blocked'));
|
||||
|
||||
const drawerParam = searchParams.get('drawer');
|
||||
const drawer: DrawerState = drawerParam && VALID_DRAWERS.includes(drawerParam as DrawerState)
|
||||
|
|
@ -73,7 +127,7 @@ export function parseUrlState(searchParams: URLSearchParams): {
|
|||
? (graphTabParam as GraphTabType)
|
||||
: DEFAULT_GRAPH_TAB;
|
||||
|
||||
return { view, taskId, swarmId, agentId, epicId, panel, drawer, graphTab };
|
||||
return { view, taskId, swarmId, agentId, epicId, leftPanel, rightPanel, blockedOnly, panel, drawer, graphTab };
|
||||
}
|
||||
|
||||
export function buildUrlParams(
|
||||
|
|
@ -97,8 +151,27 @@ export function buildUrlParams(
|
|||
export function useUrlState(): UrlState {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [panelDefaults, setPanelDefaults] = useState<PanelDefaults>({
|
||||
leftPanel: DEFAULT_LEFT_PANEL,
|
||||
rightPanel: DEFAULT_RIGHT_PANEL,
|
||||
});
|
||||
|
||||
const state = useMemo(() => parseUrlState(searchParams), [searchParams]);
|
||||
useEffect(() => {
|
||||
setPanelDefaults({
|
||||
leftPanel: readStoredPanelState(PANEL_STORAGE_KEYS.left, DEFAULT_LEFT_PANEL),
|
||||
rightPanel: readStoredPanelState(PANEL_STORAGE_KEYS.right, DEFAULT_RIGHT_PANEL),
|
||||
});
|
||||
}, []);
|
||||
|
||||
const state = useMemo(() => parseUrlState(searchParams, panelDefaults), [searchParams, panelDefaults]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(PANEL_STORAGE_KEYS.left, state.leftPanel);
|
||||
window.localStorage.setItem(PANEL_STORAGE_KEYS.right, state.rightPanel);
|
||||
}, [state.leftPanel, state.rightPanel]);
|
||||
|
||||
const updateUrl = useCallback((updates: Record<string, string | null>) => {
|
||||
const newUrl = buildUrlParams(searchParams, updates);
|
||||
|
|
@ -109,26 +182,55 @@ export function useUrlState(): UrlState {
|
|||
updateUrl({ view: v });
|
||||
}, [updateUrl]);
|
||||
|
||||
const setTaskId = useCallback((id: string | null) => {
|
||||
updateUrl({ task: id, panel: id ? 'open' : null });
|
||||
const setLeftPanel = useCallback((next: PanelState) => {
|
||||
updateUrl({ left: next });
|
||||
}, [updateUrl]);
|
||||
|
||||
const setSwarmId = useCallback((id: string | null) => {
|
||||
updateUrl({ swarm: id, panel: id ? 'open' : null });
|
||||
const toggleLeftPanel = useCallback(() => {
|
||||
setLeftPanel(state.leftPanel === 'open' ? 'closed' : 'open');
|
||||
}, [setLeftPanel, state.leftPanel]);
|
||||
|
||||
const setRightPanel = useCallback((next: PanelState) => {
|
||||
// Keep legacy `panel` in sync while migrating to explicit `right`.
|
||||
updateUrl({ right: next, panel: next });
|
||||
}, [updateUrl]);
|
||||
|
||||
const toggleRightPanel = useCallback(() => {
|
||||
setRightPanel(state.rightPanel === 'open' ? 'closed' : 'open');
|
||||
}, [setRightPanel, state.rightPanel]);
|
||||
|
||||
const setBlockedOnly = useCallback((enabled: boolean) => {
|
||||
updateUrl({ blocked: enabled ? '1' : null });
|
||||
}, [updateUrl]);
|
||||
|
||||
const toggleBlockedOnly = useCallback(() => {
|
||||
setBlockedOnly(!state.blockedOnly);
|
||||
}, [setBlockedOnly, state.blockedOnly]);
|
||||
|
||||
const setTaskId = useCallback((id: string | null, openDrawer?: boolean) => {
|
||||
const right = id ? 'open' : null;
|
||||
const drawer = openDrawer ? 'open' : null;
|
||||
// Clear swarm when setting task
|
||||
updateUrl({ task: id, swarm: null, right, panel: right, drawer });
|
||||
}, [updateUrl]);
|
||||
|
||||
const setSwarmId = useCallback((id: string | null, openDrawer?: boolean) => {
|
||||
const right = id ? 'open' : null;
|
||||
const drawer = openDrawer ? 'open' : null;
|
||||
// Clear task when setting swarm
|
||||
updateUrl({ swarm: id, task: null, right, panel: right, drawer });
|
||||
}, [updateUrl]);
|
||||
|
||||
const setAgentId = useCallback((id: string | null) => {
|
||||
updateUrl({ agent: id, panel: id ? 'open' : null });
|
||||
const right = id ? 'open' : null;
|
||||
updateUrl({ agent: id, right, panel: right });
|
||||
}, [updateUrl]);
|
||||
|
||||
const setEpicId = useCallback((id: string | null) => {
|
||||
updateUrl({ epic: id });
|
||||
}, [updateUrl]);
|
||||
|
||||
const togglePanel = useCallback(() => {
|
||||
const newPanel = state.panel === 'open' ? 'closed' : 'open';
|
||||
updateUrl({ panel: newPanel });
|
||||
}, [state.panel, updateUrl]);
|
||||
const togglePanel = toggleRightPanel;
|
||||
|
||||
const setDrawer = useCallback((state: DrawerState) => {
|
||||
updateUrl({ drawer: state });
|
||||
|
|
@ -139,7 +241,7 @@ export function useUrlState(): UrlState {
|
|||
}, [updateUrl]);
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
updateUrl({ task: null, swarm: null, epic: null, panel: 'closed', drawer: 'closed' });
|
||||
updateUrl({ task: null, swarm: null, epic: null, right: 'closed', panel: 'closed', drawer: 'closed' });
|
||||
}, [updateUrl]);
|
||||
|
||||
return {
|
||||
|
|
@ -153,7 +255,16 @@ export function useUrlState(): UrlState {
|
|||
setAgentId,
|
||||
epicId: state.epicId,
|
||||
setEpicId,
|
||||
panel: state.panel,
|
||||
leftPanel: state.leftPanel,
|
||||
setLeftPanel,
|
||||
toggleLeftPanel,
|
||||
rightPanel: state.rightPanel,
|
||||
setRightPanel,
|
||||
toggleRightPanel,
|
||||
blockedOnly: state.blockedOnly,
|
||||
setBlockedOnly,
|
||||
toggleBlockedOnly,
|
||||
panel: state.rightPanel,
|
||||
togglePanel,
|
||||
drawer: state.drawer,
|
||||
setDrawer,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue