feat: sync pane workspace overhaul from private repo

- ship the expanded left-nav and pane header workspace updates
- add dimension-driven browse flow, map view upgrades, and delete-node tooling
- expand eval coverage and refresh the public UI docs for the new layout

Generated with Codex
This commit is contained in:
“BeeRad”
2026-03-17 18:20:20 +11:00
parent 0776bccdbd
commit 213c054c9f
25 changed files with 1050 additions and 630 deletions
+89 -178
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useCallback } from 'react';
import { useState, type ReactNode } from 'react';
import {
Search,
Plus,
@@ -11,6 +11,8 @@ import {
Table2,
BookOpen,
Settings,
PanelLeftClose,
PanelLeftOpen,
} from 'lucide-react';
import type { PaneType } from '../panes/types';
@@ -20,127 +22,74 @@ interface LeftToolbarProps {
onRefreshClick: () => void;
onSettingsClick: () => void;
onPaneTypeClick: (paneType: PaneType) => void;
activePane: 'A' | 'B';
slotAType: PaneType | null;
slotBType: PaneType | null;
isExpanded: boolean;
onToggleExpanded: () => void;
openTabTypes: Set<PaneType>;
activeTabType: PaneType | null;
}
// Map pane types to their icons (chat removed in rah-light, guides moved to settings)
const PANE_TYPE_ICONS: Record<string, typeof LayoutList> = {
views: LayoutList,
map: Map,
dimensions: Folder,
table: Table2,
skills: BookOpen,
};
const NAV_WIDTH_COLLAPSED = 50;
const NAV_WIDTH_EXPANDED = 280;
const PANE_TYPE_LABELS: Record<string, string> = {
views: 'Feed',
map: 'Map',
dimensions: 'Dimensions',
table: 'Table',
skills: 'Skills',
};
const VIEW_ITEMS: Array<{ paneType: PaneType; label: string; icon: typeof LayoutList }> = [
{ paneType: 'views', label: 'Nodes', icon: LayoutList },
{ paneType: 'skills', label: 'Skills', icon: BookOpen },
{ paneType: 'map', label: 'Map', icon: Map },
{ paneType: 'dimensions', label: 'Dimension', icon: Folder },
{ paneType: 'table', label: 'Table', icon: Table2 },
];
// Pane types shown in the toolbar center section (skills is pinned above settings)
const TOOLBAR_PANE_TYPES: PaneType[] = ['views', 'map', 'dimensions', 'table'];
interface ToolbarButtonProps {
function NavButton({
icon: Icon,
label,
expanded,
active,
onClick,
trailing,
activeTone = 'neutral',
}: {
icon: typeof Search;
label: string;
shortcut?: string;
expanded: boolean;
active?: boolean;
onClick: () => void;
disabled?: boolean;
isActive?: boolean;
}
function ToolbarButton({ icon: Icon, label, shortcut, onClick, disabled, isActive }: ToolbarButtonProps) {
const [isHovered, setIsHovered] = useState(false);
trailing?: ReactNode;
activeTone?: 'neutral' | 'green';
}) {
const [hovered, setHovered] = useState(false);
const activeColor = activeTone === 'green' ? '#22c55e' : '#f0f0f0';
return (
<button
onClick={onClick}
disabled={disabled}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
title={shortcut ? `${label} (${shortcut})` : label}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
title={expanded ? undefined : label}
style={{
width: '36px',
width: '100%',
height: '36px',
borderRadius: '8px',
border: 'none',
background: isHovered ? '#1a1a1a' : 'transparent',
color: isActive ? '#22c55e' : (isHovered ? '#aaa' : '#666'),
cursor: disabled ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.15s ease',
opacity: disabled ? 0.5 : 1,
}}
>
<Icon size={18} />
</button>
);
}
interface PaneTypeButtonProps {
icon: typeof LayoutList;
label: string;
paneType: PaneType;
isOpen: boolean;
isActivePane: boolean;
onClick: () => void;
}
function PaneTypeButton({ icon: Icon, label, paneType, isOpen, isActivePane, onClick }: PaneTypeButtonProps) {
const [isHovered, setIsHovered] = useState(false);
// Determine color: green if open, brighter if it's the active pane
const getColor = () => {
if (isOpen) {
return isActivePane ? '#4ade80' : '#22c55e'; // Brighter green for active
}
return isHovered ? '#aaa' : '#666';
};
return (
<button
onClick={onClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
title={label}
style={{
width: '36px',
height: '36px',
borderRadius: '8px',
border: 'none',
background: isOpen ? '#1a1a1a' : (isHovered ? '#151515' : 'transparent'),
color: getColor(),
background: active ? '#151515' : (hovered ? '#121212' : 'transparent'),
color: active ? activeColor : (hovered ? '#c7c7c7' : '#7a7a7a'),
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
justifyContent: expanded ? 'space-between' : 'center',
gap: '10px',
padding: expanded ? '0 10px' : '0',
transition: 'all 0.15s ease',
position: 'relative',
}}
>
<Icon size={18} />
{/* Active pane indicator dot */}
{isActivePane && isOpen && (
<div
style={{
position: 'absolute',
bottom: '4px',
left: '50%',
transform: 'translateX(-50%)',
width: '4px',
height: '4px',
borderRadius: '50%',
background: '#4ade80',
}}
/>
)}
<span style={{ display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0 }}>
<Icon size={18} />
{expanded ? (
<span style={{ fontSize: '13px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{label}
</span>
) : null}
</span>
{expanded ? trailing : null}
</button>
);
}
@@ -151,97 +100,59 @@ export default function LeftToolbar({
onRefreshClick,
onSettingsClick,
onPaneTypeClick,
activePane,
slotAType,
slotBType,
isExpanded,
onToggleExpanded,
openTabTypes,
activeTabType,
}: LeftToolbarProps) {
// Determine which pane types are currently open
const openPaneTypes = new Set<PaneType>(
[slotAType, slotBType].filter((t): t is PaneType => t !== null)
);
// Determine which pane type is in the active pane (null if pane is closed)
const activePaneType = activePane === 'A' ? slotAType : slotBType;
return (
<div
style={{
width: '50px',
width: isExpanded ? `${NAV_WIDTH_EXPANDED}px` : `${NAV_WIDTH_COLLAPSED}px`,
height: '100%',
background: '#0a0a0a',
background: 'transparent',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '12px 0',
padding: '12px 8px',
flexShrink: 0,
transition: 'width 0.2s ease',
overflow: 'hidden',
}}
>
{/* Top section - Actions */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<ToolbarButton
icon={Search}
label="Search"
shortcut="⌘K"
onClick={onSearchClick}
/>
<ToolbarButton
icon={Plus}
label="Add Stuff"
onClick={onAddStuffClick}
/>
<ToolbarButton
icon={RefreshCw}
label="Refresh"
shortcut="⌘⇧R"
onClick={onRefreshClick}
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
<NavButton
icon={isExpanded ? PanelLeftClose : PanelLeftOpen}
label={isExpanded ? 'Collapse' : 'Expand'}
expanded={isExpanded}
onClick={onToggleExpanded}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<NavButton icon={Search} label="Search" expanded={isExpanded} onClick={onSearchClick} />
<NavButton icon={Plus} label="Add Stuff" expanded={isExpanded} onClick={onAddStuffClick} />
<NavButton icon={RefreshCw} label="Refresh" expanded={isExpanded} onClick={onRefreshClick} />
</div>
<div style={{ borderTop: '1px solid #1a1a1a', paddingTop: '14px', marginTop: '10px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', paddingTop: '4px' }}>
{VIEW_ITEMS.map((item) => (
<NavButton
key={`${item.paneType}-${item.label}`}
icon={item.icon}
label={item.label}
expanded={isExpanded}
active={activeTabType === item.paneType || openTabTypes.has(item.paneType)}
onClick={() => onPaneTypeClick(item.paneType)}
activeTone="green"
/>
))}
</div>
</div>
</div>
{/* Middle section - Pane Types */}
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '4px',
padding: '8px 0',
}}
>
{TOOLBAR_PANE_TYPES.map((paneType) => {
const Icon = PANE_TYPE_ICONS[paneType];
const label = PANE_TYPE_LABELS[paneType];
const isOpen = openPaneTypes.has(paneType);
const isActivePane = activePaneType === paneType;
return (
<PaneTypeButton
key={paneType}
icon={Icon}
label={label}
paneType={paneType}
isOpen={isOpen}
isActivePane={isActivePane}
onClick={() => onPaneTypeClick(paneType)}
/>
);
})}
</div>
{/* Bottom section - Skills + Settings */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<PaneTypeButton
icon={BookOpen}
label="Skills"
paneType="skills"
isOpen={openPaneTypes.has('skills')}
isActivePane={activePaneType === 'skills'}
onClick={() => onPaneTypeClick('skills')}
/>
<ToolbarButton
icon={Settings}
label="Settings"
onClick={onSettingsClick}
/>
<div style={{ borderTop: '1px solid #1a1a1a', paddingTop: '8px' }}>
<NavButton icon={Settings} label="Settings" expanded={isExpanded} onClick={onSettingsClick} />
</div>
</div>
);
+70 -40
View File
@@ -45,16 +45,17 @@ export default function ThreePanelLayout() {
// Slot states - the core of the flexible pane system
// Default: Feed on left, closed on right (chat removed in rah-light)
const [slotA, setSlotA] = usePersistentState<SlotState | null>('ui.slotA.v4', {
const [slotA, setSlotA] = usePersistentState<SlotState | null>('ui.slotA.v5', {
type: 'views',
});
// SlotB can be null (closed) or a SlotState
// Default: closed (chat removed in rah-light)
const [slotB, setSlotB] = usePersistentState<SlotState | null>('ui.slotB.v4', null);
const [slotB, setSlotB] = usePersistentState<SlotState | null>('ui.slotB.v5', null);
// SlotB width as percentage (when open)
const [slotBWidth, setSlotBWidth] = usePersistentState<number>('ui.slotBWidth', 50);
const [leftNavExpanded, setLeftNavExpanded] = usePersistentState<boolean>('ui.leftNavExpanded', false);
// Migration: if a slot was persisted with type 'guides' (now moved to settings), reset it
useEffect(() => {
@@ -64,6 +65,12 @@ export default function ThreePanelLayout() {
if (slotB && (slotB.type as string) === 'guides') {
setSlotB(null);
}
if (slotA?.type === 'node') {
setSlotA({ type: 'views' });
}
if (slotB?.type === 'node') {
setSlotB(null);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// Track which pane is active (last interacted with)
@@ -96,6 +103,7 @@ export default function ThreePanelLayout() {
// Active dimension tracking
const [activeDimension, setActiveDimension] = usePersistentState<string | null>('ui.focus.activeDimension', null);
const [browseDimensionFilter, setBrowseDimensionFilter] = useState<string | null>(null);
// Delegations state (deprecated - kept for component compatibility)
const [delegationsMap] = useState<Record<string, AgentDelegation>>({});
@@ -485,6 +493,37 @@ export default function ThreePanelLayout() {
setNodesPanelRefresh(prev => prev + 1);
}, []);
const openPaneSingleton = useCallback((paneType: PaneType) => {
if (paneType !== 'node') {
if (slotA?.type === paneType) {
setActivePane('A');
return;
}
if (slotB?.type === paneType) {
setActivePane('B');
return;
}
}
if (!slotA) {
setSlotA({ type: paneType });
setActivePane('A');
return;
}
if (!slotB) {
setSlotB({ type: paneType });
setActivePane('B');
return;
}
if (activePane === 'A') {
setSlotA(prev => prev ? ({ ...prev, type: paneType }) : { type: paneType });
} else {
setSlotB(prev => prev ? ({ ...prev, type: paneType }) : { type: paneType });
}
}, [activePane, slotA, slotB, setSlotA, setSlotB]);
const handleNodeOpenFromDimensions = useCallback((nodeId: number) => {
// Switch to node pane and open the node
const currentTabs = slotA?.type === 'node' ? (slotA.nodeTabs || []) : [];
@@ -499,35 +538,26 @@ export default function ThreePanelLayout() {
setActivePane('A');
}, [slotA, setSlotA]);
// Handle pane type selection from toolbar
const handlePaneTypeClick = useCallback((paneType: PaneType) => {
// If no panes open → open in slot A
if (!slotA) {
setSlotA({ type: paneType });
setActivePane('A');
return;
}
const handleDimensionPaneSelect = useCallback((dimensionName: string | null) => {
setBrowseDimensionFilter(dimensionName);
setActiveDimension(dimensionName);
// If only one pane open → open second pane with this type
if (!slotB) {
setSlotB({ type: paneType });
if (!dimensionName) return;
if (activePane === 'B' && slotB?.type === 'dimensions') {
setSlotB({ type: 'views' });
setActivePane('B');
return;
}
// Two panes open → replace the active pane
if (activePane === 'A') {
setSlotA(prev => prev ? ({
...prev,
type: paneType,
}) : { type: paneType });
} else {
setSlotB(prev => prev ? ({
...prev,
type: paneType,
}) : { type: paneType });
}
}, [activePane, slotA, slotB, setSlotA, setSlotB]);
setSlotA({ type: 'views' });
setActivePane('A');
}, [activePane, slotB, setSlotA, setSlotB, setActiveDimension]);
// Handle pane type selection from toolbar
const handlePaneTypeClick = useCallback((paneType: PaneType) => {
openPaneSingleton(paneType);
}, [openPaneSingleton]);
// Ensure the Feed pane is visible (for quick-add loading placeholders)
const ensureFeedOpen = useCallback(() => {
@@ -596,25 +626,19 @@ export default function ThreePanelLayout() {
const handleSlotAAction = useCallback((action: PaneAction) => {
switch (action.type) {
case 'switch-pane-type':
setSlotA(prev => ({
...prev,
type: action.paneType,
}));
openPaneSingleton(action.paneType);
break;
case 'open-node':
handleNodeSelect(action.nodeId, false);
break;
}
}, [handleNodeSelect, setSlotA]);
}, [handleNodeSelect, openPaneSingleton]);
const handleSlotBAction = useCallback((action: PaneAction) => {
if (!slotB) return;
switch (action.type) {
case 'switch-pane-type':
setSlotB(prev => prev ? ({
...prev,
type: action.paneType,
}) : null);
openPaneSingleton(action.paneType);
break;
case 'open-node':
// Open node in slot B (if it's a node pane)
@@ -636,7 +660,7 @@ export default function ThreePanelLayout() {
}
break;
}
}, [slotB, setSlotB]);
}, [slotB, setSlotB, openPaneSingleton]);
// Open a node directly in Slot B (for Alt+Click)
const handleNodeOpenInSlotB = useCallback((nodeId: number) => {
@@ -909,7 +933,7 @@ export default function ThreePanelLayout() {
onNodeOpen={handleNodeOpenFromDimensions}
refreshToken={folderViewRefresh}
onDataChanged={handleFolderViewDataChanged}
onDimensionSelect={setActiveDimension}
onDimensionSelect={handleDimensionPaneSelect}
/>
);
@@ -942,6 +966,11 @@ export default function ThreePanelLayout() {
refreshToken={nodesPanelRefresh}
pendingNodes={pendingNodes}
onDismissPending={(id) => setPendingNodes(prev => prev.filter(p => p.id !== id))}
externalDimensionFilter={browseDimensionFilter}
onClearExternalDimensionFilter={() => {
setBrowseDimensionFilter(null);
setActiveDimension(null);
}}
/>
);
@@ -998,9 +1027,10 @@ export default function ThreePanelLayout() {
setShowSettings(true);
}}
onPaneTypeClick={handlePaneTypeClick}
activePane={activePane}
slotAType={slotA?.type ?? null}
slotBType={slotB?.type ?? null}
isExpanded={leftNavExpanded}
onToggleExpanded={() => setLeftNavExpanded(prev => !prev)}
openTabTypes={new Set([slotA?.type, slotB?.type].filter((t): t is PaneType => t != null))}
activeTabType={activePane === 'A' ? slotA?.type ?? null : slotB?.type ?? null}
onRefreshClick={handleRefreshAll}
/>