feat: port contexts layer and MCP parity

This commit is contained in:
“BeeRad”
2026-04-10 19:41:26 +10:00
parent a8c5506fb7
commit 35f9ecf89c
62 changed files with 4206 additions and 1224 deletions
File diff suppressed because it is too large Load Diff
+80
View File
@@ -8,6 +8,8 @@ import {
LayoutList,
Map,
Folder,
ChevronDown,
ChevronRight,
Table2,
BookOpen,
Settings,
@@ -18,6 +20,7 @@ import {
} from 'lucide-react';
import type { PaneType, NavigablePaneType } from '../panes/types';
import type { Theme } from '@/hooks/useTheme';
import type { ContextSummary } from '@/types/database';
interface LeftToolbarProps {
onSearchClick: () => void;
@@ -31,6 +34,8 @@ interface LeftToolbarProps {
activeTabType: PaneType | null;
theme: Theme;
onThemeToggle: () => void;
contexts?: ContextSummary[];
onContextQuickSelect?: (contextId: number, contextName: string) => void;
}
const NAV_WIDTH_COLLAPSED = 50;
@@ -113,7 +118,11 @@ export default function LeftToolbar({
activeTabType,
theme,
onThemeToggle,
contexts = [],
onContextQuickSelect,
}: LeftToolbarProps) {
const [contextsExpanded, setContextsExpanded] = useState(false);
return (
<div
style={{
@@ -148,6 +157,77 @@ export default function LeftToolbar({
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', minHeight: 0 }}>
<div style={{ borderTop: '1px solid var(--rah-border)', paddingTop: '14px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', marginBottom: '6px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<div style={{ flex: 1, minWidth: 0 }}>
<NavButton
icon={Folder}
label="Contexts"
expanded={isExpanded}
active={activeTabType === 'contexts' || openTabTypes.has('contexts')}
onClick={() => onPaneTypeClick('contexts')}
activeTone="green"
/>
</div>
{isExpanded ? (
<button
type="button"
onClick={() => setContextsExpanded((prev) => !prev)}
style={{
width: '28px',
height: '36px',
borderRadius: '8px',
border: 'none',
background: contextsExpanded ? 'var(--rah-bg-hover)' : 'transparent',
color: contextsExpanded ? 'var(--rah-text-active)' : 'var(--rah-text-muted)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
flexShrink: 0,
}}
title="Toggle context list"
>
{contextsExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
) : null}
</div>
{isExpanded && contextsExpanded && contexts.length > 0 ? (
<div style={{ marginLeft: '14px', paddingLeft: '12px', borderLeft: '1px solid var(--rah-border)', display: 'flex', flexDirection: 'column', gap: '2px' }}>
{contexts.map((context) => (
<button
key={context.id}
type="button"
onClick={() => onContextQuickSelect?.(context.id, context.name)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '8px',
width: '100%',
minHeight: '28px',
border: 'none',
background: 'transparent',
color: 'var(--rah-text-secondary)',
cursor: 'pointer',
padding: '0 8px',
borderRadius: '8px',
textAlign: 'left',
}}
>
<span style={{ fontSize: '12px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{context.name}
</span>
<span style={{ fontSize: '10px', color: 'var(--rah-text-muted)', flexShrink: 0 }}>
{context.count}
</span>
</button>
))}
</div>
) : null}
</div>
{VIEW_ITEMS.map((item) => (
<NavButton
key={`${item.paneType}-${item.label}`}
+78 -6
View File
@@ -4,7 +4,7 @@ import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { PanelLeftOpen, GripVertical, X } from 'lucide-react';
import SettingsModal, { SettingsTab } from '../settings/SettingsModal';
import SearchModal from '../nodes/SearchModal';
import { Node } from '@/types/database';
import { ContextSummary, Node } from '@/types/database';
import { DatabaseEvent } from '@/services/events';
import { usePersistentState } from '@/hooks/usePersistentState';
import { useTheme } from '@/hooks/useTheme';
@@ -12,7 +12,7 @@ import { useTheme } from '@/hooks/useTheme';
import LeftToolbar from './LeftToolbar';
import SplitHandle from './SplitHandle';
import { NodePane, DimensionsPane, MapPane, ViewsPane, TablePane, SkillsPane } from '../panes';
import { NodePane, ContextsPane, DimensionsPane, MapPane, ViewsPane, TablePane, SkillsPane } from '../panes';
import QuickAddInput from '../agents/QuickAddInput';
import type { PaneType, SlotState, PaneAction, SlotId } from '../panes/types';
@@ -35,10 +35,11 @@ const PANEL_A_WEIGHT_KEY = 'ui.panelA.weight.v1';
const PANEL_B_WEIGHT_KEY = 'ui.panelB.weight.v1';
const PANEL_C_WEIGHT_KEY = 'ui.panelC.weight.v1';
const LEFT_NAV_EXPANDED_KEY = 'ui.leftNavExpanded';
const ACTIVE_CONTEXT_KEY = 'ui.focus.activeContextId';
const ACTIVE_DIMENSION_KEY = 'ui.focus.activeDimension';
const DEFAULT_SLOT_A: SlotState = { type: 'views' };
const VALID_PANE_TYPES = new Set<PaneType>(['node', 'dimensions', 'map', 'views', 'table', 'skills']);
const VALID_PANE_TYPES = new Set<PaneType>(['node', 'contexts', 'dimensions', 'map', 'views', 'table', 'skills']);
function normalizeSlotState(raw: SlotState | null): SlotState | null {
if (!raw) return null;
@@ -84,7 +85,14 @@ export default function ThreePanelLayout() {
const [nodesPanelRefresh, setNodesPanelRefresh] = useState(0);
const [focusPanelRefresh, setFocusPanelRefresh] = useState(0);
const [folderViewRefresh, setFolderViewRefresh] = useState(0);
const [availableContexts, setAvailableContexts] = useState<ContextSummary[]>([]);
const [activeContextId, setActiveContextId] = usePersistentState<number | null>(ACTIVE_CONTEXT_KEY, null);
const [activeDimension, setActiveDimension] = usePersistentState<string | null>(ACTIVE_DIMENSION_KEY, null);
const [browseContextFilters, setBrowseContextFilters] = useState<Record<SlotId, number | null>>({
A: null,
B: null,
C: null,
});
const [browseDimensionFilters, setBrowseDimensionFilters] = useState<Record<SlotId, string | null>>({
A: null,
B: null,
@@ -104,6 +112,20 @@ export default function ThreePanelLayout() {
setSlotC((prev) => normalizeSlotState(prev));
}, [setSlotA, setSlotB, setSlotC]);
useEffect(() => {
void (async () => {
try {
const response = await fetch('/api/contexts');
const data = await response.json();
if (response.ok && data.success) {
setAvailableContexts(data.data || []);
}
} catch (error) {
console.error('Failed to fetch contexts:', error);
}
})();
}, [nodesPanelRefresh, folderViewRefresh]);
const getSlotState = useCallback((slot: SlotId): SlotState | null => {
switch (slot) {
case 'A':
@@ -549,6 +571,17 @@ export default function ThreePanelLayout() {
openNodeFromSlot(nodeId, 'A');
}, [openNodeFromSlot]);
const handleContextSelect = useCallback((slot: SlotId, contextId: number | null, _contextName?: string | null) => {
setBrowseContextFilters((prev) => ({ ...prev, [slot]: contextId }));
setActiveContextId(contextId);
if (contextId == null) return;
setPanelExpanded(slot, true);
getSlotSetter(slot)({ type: 'views' });
setActivePane(slot);
}, [getSlotSetter, setActiveContextId, setPanelExpanded]);
const handleDimensionPaneSelect = useCallback((slot: SlotId, dimensionName: string | null) => {
setBrowseDimensionFilters((prev) => ({ ...prev, [slot]: dimensionName }));
setActiveDimension(dimensionName);
@@ -565,7 +598,7 @@ export default function ThreePanelLayout() {
const response = await fetch('/api/quick-add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input, mode, description }),
body: JSON.stringify({ input, mode, description, contextId: activeContextId }),
});
if (!response.ok) {
@@ -591,7 +624,7 @@ export default function ThreePanelLayout() {
} catch (error) {
console.error('[ThreePanelLayout] Quick Add error:', error);
}
}, [openPaneSingleton]);
}, [activeContextId, openPaneSingleton]);
const handleCloseSlotA = useCallback(() => {
setSlotA(null);
@@ -619,11 +652,20 @@ export default function ThreePanelLayout() {
setActivePane(slot);
}
break;
case 'open-context':
handleContextSelect(action.targetSlot ?? slot, action.contextId, action.contextName);
break;
case 'open-dimension':
setBrowseDimensionFilters((prev) => ({ ...prev, [action.targetSlot ?? slot]: action.dimension }));
setActiveDimension(action.dimension);
setSingletonPaneInSlot(action.targetSlot ?? slot, 'views');
setActivePane(action.targetSlot ?? slot);
break;
case 'open-node':
openNodeFromSlot(action.nodeId, slot);
break;
}
}, [openNodeFromSlot, setSingletonPaneInSlot]);
}, [handleContextSelect, openNodeFromSlot, setActiveDimension, setSingletonPaneInSlot]);
const handleSearchNodeSelect = useCallback((nodeId: number) => {
handleNodeSelect(nodeId, false);
@@ -758,6 +800,18 @@ export default function ThreePanelLayout() {
/>
);
case 'contexts':
return (
<ContextsPane
slot={slot}
isActive={isActive}
onPaneAction={(action) => handleSlotAction(slot, action)}
onCollapse={onCollapse}
onSwapPanes={handleSwapPanes}
onContextSelect={(contextId, contextName) => handleContextSelect(slot, contextId, contextName)}
/>
);
case 'dimensions':
return (
<DimensionsPane
@@ -802,7 +856,16 @@ export default function ThreePanelLayout() {
refreshToken={nodesPanelRefresh}
pendingNodes={pendingNodes}
onDismissPending={(id) => setPendingNodes(prev => prev.filter(p => p.id !== id))}
externalContextFilterId={browseContextFilters[slot]}
externalDimensionFilter={browseDimensionFilters[slot]}
onContextFilterSelect={(contextId) => {
setBrowseContextFilters((prev) => ({ ...prev, [slot]: contextId }));
setActiveContextId(contextId);
}}
onClearExternalContextFilter={() => {
setBrowseContextFilters((prev) => ({ ...prev, [slot]: null }));
setActiveContextId(null);
}}
onClearExternalDimensionFilter={() => {
setBrowseDimensionFilters((prev) => ({ ...prev, [slot]: null }));
setActiveDimension(null);
@@ -985,6 +1048,15 @@ export default function ThreePanelLayout() {
onRefreshClick={handleRefreshAll}
theme={theme}
onThemeToggle={toggleTheme}
contexts={availableContexts}
onContextQuickSelect={(contextId, contextName) => {
const target = openPaneSingleton('views', 'A');
setBrowseContextFilters((prev) => ({ ...prev, [target]: contextId }));
setBrowseDimensionFilters((prev) => ({ ...prev, [target]: null }));
setActiveContextId(contextId);
setActivePane(target);
void contextName;
}}
/>
<div
+124
View File
@@ -0,0 +1,124 @@
"use client";
import { useEffect, useState } from 'react';
import { Folder } from 'lucide-react';
import type { ContextSummary } from '@/types/database';
import PaneHeader from './PaneHeader';
import type { ContextsPaneProps } from './types';
export default function ContextsPane({
slot,
onCollapse,
onSwapPanes,
tabBar,
onContextSelect,
}: ContextsPaneProps) {
const [contexts, setContexts] = useState<ContextSummary[]>([]);
const [loading, setLoading] = useState(true);
const [toolbarHost, setToolbarHost] = useState<HTMLDivElement | null>(null);
useEffect(() => {
void (async () => {
setLoading(true);
try {
const response = await fetch('/api/contexts');
const payload = await response.json();
if (response.ok && payload.success) {
setContexts(payload.data || []);
}
} finally {
setLoading(false);
}
})();
}, []);
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', overflow: 'hidden' }}>
<PaneHeader
slot={slot}
onCollapse={onCollapse}
onSwapPanes={onSwapPanes}
tabBar={tabBar}
toolbarHostRef={setToolbarHost}
/>
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px' }}>
<div style={{ maxWidth: '980px', margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '8px' }}>
{loading ? (
<div style={emptyStateStyle}>Loading contexts...</div>
) : contexts.length === 0 ? (
<div style={emptyStateStyle}>No contexts yet.</div>
) : (
contexts.map((context) => (
<button
key={context.id}
type="button"
onClick={() => onContextSelect?.(context.id, context.name)}
style={rowStyle}
>
<div style={iconWrapStyle}>
<Folder size={17} />
</div>
<div style={{ minWidth: 0, flex: 1, textAlign: 'left' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0, flexWrap: 'wrap' }}>
<span style={{ fontSize: '14px', fontWeight: 600, color: 'var(--rah-text-primary)' }}>{context.name}</span>
<span style={countStyle}>{context.count}</span>
</div>
{context.description ? (
<div style={{ marginTop: '4px', fontSize: '12px', color: 'var(--rah-text-secondary)', lineHeight: 1.45 }}>
{context.description}
</div>
) : null}
</div>
</button>
))
)}
</div>
</div>
</div>
);
}
const rowStyle: React.CSSProperties = {
width: '100%',
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '12px 14px',
border: '1px solid var(--rah-border)',
borderRadius: '12px',
background: 'var(--rah-bg-card)',
cursor: 'pointer',
};
const iconWrapStyle: React.CSSProperties = {
width: '32px',
height: '32px',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '10px',
border: '1px solid var(--rah-border)',
background: 'var(--rah-bg-panel)',
color: 'var(--rah-text-muted)',
flexShrink: 0,
};
const countStyle: React.CSSProperties = {
fontSize: '10px',
lineHeight: 1.2,
padding: '3px 7px',
borderRadius: '999px',
border: '1px solid var(--rah-border)',
background: 'var(--rah-bg-panel)',
color: 'var(--rah-text-muted)',
};
const emptyStateStyle: React.CSSProperties = {
border: '1px dashed var(--rah-border-strong)',
borderRadius: '12px',
padding: '16px',
color: 'var(--rah-text-muted)',
fontSize: '13px',
background: 'var(--rah-bg-card)',
};
+76 -78
View File
@@ -17,6 +17,7 @@ export default function NodePane({
onPaneAction,
onCollapse,
onSwapPanes,
tabBar,
openTabs,
activeTab,
onTabSelect,
@@ -92,76 +93,76 @@ export default function NodePane({
background: 'transparent',
overflow: 'hidden',
}}>
<PaneHeader slot={slot} onCollapse={onCollapse} onSwapPanes={onSwapPanes}>
{/* Tabs rendered inline */}
{openTabs.length === 0 ? (
<span style={{ fontSize: '12px', color: 'var(--rah-text-muted)' }}>No tabs open</span>
) : (
openTabs.map((tabId) => {
const title = nodeTitles[tabId] || 'Loading...';
const isActiveTab = activeTab === tabId;
return (
<div
key={tabId}
draggable
onDragStart={(e) => {
e.dataTransfer.effectAllowed = 'copyMove';
e.dataTransfer.setData('application/x-rah-tab', JSON.stringify({ id: tabId, title, sourceSlot: slot }));
e.dataTransfer.setData('text/plain', `[NODE:${tabId}:"${title}"]`);
}}
onContextMenu={(e) => {
e.preventDefault();
setContextMenu({ x: e.clientX, y: e.clientY, tabId });
}}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '4px 8px',
background: isActiveTab ? 'var(--rah-bg-active)' : 'transparent',
borderRadius: '4px',
cursor: 'grab',
flexShrink: 0,
}}
>
<button
onClick={() => onTabSelect(tabId)}
style={{
fontSize: '11px',
color: isActiveTab ? 'var(--rah-text-active)' : 'var(--rah-text-muted)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 0,
whiteSpace: 'nowrap',
<PaneHeader slot={slot} onCollapse={onCollapse} onSwapPanes={onSwapPanes} tabBar={tabBar ? tabBar : (
/* Legacy node tabs (fallback when no tabBar prop) */
openTabs.length === 0 ? (
<span style={{ fontSize: '12px', color: '#666' }}>No tabs open</span>
) : (
openTabs.map((tabId) => {
const title = nodeTitles[tabId] || 'Loading...';
const isActiveTab = activeTab === tabId;
return (
<div
key={tabId}
draggable
onDragStart={(e) => {
e.dataTransfer.effectAllowed = 'copyMove';
e.dataTransfer.setData('application/x-rah-tab', JSON.stringify({ id: tabId, title, sourceSlot: slot }));
e.dataTransfer.setData('text/plain', `[NODE:${tabId}:"${title}"]`);
}}
>
{truncateTitle(title)}
</button>
<button
onClick={(e) => {
e.stopPropagation();
onTabClose(tabId);
onContextMenu={(e) => {
e.preventDefault();
setContextMenu({ x: e.clientX, y: e.clientY, tabId });
}}
style={{
fontSize: '12px',
color: 'var(--rah-text-muted)',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '0 2px',
lineHeight: 1,
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '4px 8px',
background: isActiveTab ? '#1f1f1f' : 'transparent',
borderRadius: '4px',
cursor: 'grab',
flexShrink: 0,
}}
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--rah-text-active)'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--rah-text-muted)'; }}
>
×
</button>
</div>
);
})
)}
</PaneHeader>
<button
onClick={() => onTabSelect(tabId)}
style={{
fontSize: '11px',
color: isActiveTab ? '#fff' : '#888',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: 0,
whiteSpace: 'nowrap',
}}
>
{truncateTitle(title)}
</button>
<button
onClick={(e) => {
e.stopPropagation();
onTabClose(tabId);
}}
style={{
fontSize: '12px',
color: '#666',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '0 2px',
lineHeight: 1,
}}
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#666'; }}
>
×
</button>
</div>
);
})
)
)} />
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
<FocusPanel
@@ -171,11 +172,8 @@ export default function NodePane({
onNodeClick={onNodeClick}
onTabClose={onTabClose}
refreshTrigger={refreshTrigger}
onReorderTabs={onReorderTabs}
onOpenInOtherSlot={onOpenInOtherSlot}
onTextSelect={onTextSelect}
highlightedPassage={highlightedPassage}
hideTabBar
/>
</div>
@@ -186,8 +184,8 @@ export default function NodePane({
position: 'fixed',
top: contextMenu.y,
left: contextMenu.x,
background: 'var(--rah-bg-active)',
border: '1px solid var(--rah-border-strong)',
background: '#1a1a1a',
border: '1px solid #2a2a2a',
borderRadius: '6px',
padding: '4px',
zIndex: 9999,
@@ -211,18 +209,18 @@ export default function NodePane({
background: 'transparent',
border: 'none',
borderRadius: '4px',
color: 'var(--rah-text-secondary)',
color: '#ccc',
fontSize: '12px',
cursor: 'pointer',
textAlign: 'left',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--rah-bg-active)';
e.currentTarget.style.color = 'var(--rah-text-active)';
e.currentTarget.style.background = '#2a2a2a';
e.currentTarget.style.color = '#fff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = 'var(--rah-text-secondary)';
e.currentTarget.style.color = '#ccc';
}}
>
<span style={{ fontSize: '14px' }}></span>
@@ -243,18 +241,18 @@ export default function NodePane({
background: 'transparent',
border: 'none',
borderRadius: '4px',
color: 'var(--rah-text-secondary)',
color: '#ccc',
fontSize: '12px',
cursor: 'pointer',
textAlign: 'left',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--rah-bg-active)';
e.currentTarget.style.color = 'var(--rah-text-active)';
e.currentTarget.style.background = '#2a2a2a';
e.currentTarget.style.color = '#fff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = 'var(--rah-text-secondary)';
e.currentTarget.style.color = '#ccc';
}}
>
<span style={{ fontSize: '14px' }}>×</span>
+9
View File
@@ -12,7 +12,10 @@ export interface ViewsPaneProps extends BasePaneProps {
refreshToken?: number;
pendingNodes?: PendingNode[];
onDismissPending?: (id: string) => void;
externalContextFilterId?: number | null;
externalDimensionFilter?: string | null;
onContextFilterSelect?: (contextId: number | null, contextName?: string | null) => void;
onClearExternalContextFilter?: () => void;
onClearExternalDimensionFilter?: () => void;
}
@@ -28,7 +31,10 @@ export default function ViewsPane({
refreshToken,
pendingNodes,
onDismissPending,
externalContextFilterId,
externalDimensionFilter,
onContextFilterSelect,
onClearExternalContextFilter,
onClearExternalDimensionFilter,
}: ViewsPaneProps) {
const [toolbarHost, setToolbarHost] = useState<HTMLDivElement | null>(null);
@@ -58,7 +64,10 @@ export default function ViewsPane({
refreshToken={refreshToken}
pendingNodes={pendingNodes}
onDismissPending={onDismissPending}
externalContextFilterId={externalContextFilterId}
externalDimensionFilter={externalDimensionFilter}
onContextFilterSelect={onContextFilterSelect}
onClearExternalContextFilter={onClearExternalContextFilter}
onClearExternalDimensionFilter={onClearExternalDimensionFilter}
toolbarHost={toolbarHost}
/>
+1
View File
@@ -1,4 +1,5 @@
export { default as NodePane } from './NodePane';
export { default as ContextsPane } from './ContextsPane';
export { default as DimensionsPane } from './DimensionsPane';
export { default as MapPane } from './MapPane';
export { default as ViewsPane } from './ViewsPane';
+10 -1
View File
@@ -18,7 +18,7 @@ export type AgentDelegation = {
};
// Pane types (chat removed in rah-light)
export type PaneType = 'node' | 'dimensions' | 'map' | 'views' | 'table' | 'skills';
export type PaneType = 'node' | 'contexts' | 'dimensions' | 'map' | 'views' | 'table' | 'skills';
// State for each slot
export interface SlotState {
@@ -34,6 +34,7 @@ export interface SlotState {
// Actions panes can emit to the layout
export type PaneAction =
| { type: 'open-node'; nodeId: number; targetSlot?: SlotId }
| { type: 'open-context'; contextId: number | null; contextName?: string | null; targetSlot?: SlotId }
| { type: 'open-dimension'; dimension: string; targetSlot?: SlotId }
| { type: 'switch-pane-type'; paneType: PaneType }
| { type: 'close-pane' };
@@ -90,10 +91,17 @@ export interface ViewsPaneProps extends BasePaneProps {
onNodeClick: (nodeId: number) => void;
onNodeOpenInOtherPane?: (nodeId: number) => void;
refreshToken?: number;
externalContextFilterId?: number | null;
externalDimensionFilter?: string | null;
onContextFilterSelect?: (contextId: number | null, contextName?: string | null) => void;
onClearExternalContextFilter?: () => void;
onClearExternalDimensionFilter?: () => void;
}
export interface ContextsPaneProps extends BasePaneProps {
onContextSelect?: (contextId: number | null, contextName?: string | null) => void;
}
// TablePane specific props
export interface TablePaneProps extends BasePaneProps {
onNodeClick: (nodeId: number) => void;
@@ -113,6 +121,7 @@ export interface PaneHeaderProps {
// Labels for pane types
export const PANE_LABELS: Record<PaneType, string> = {
node: 'Nodes',
contexts: 'Contexts',
dimensions: 'Dimensions',
map: 'Map',
views: 'Feed',
+1 -1
View File
@@ -73,7 +73,7 @@ export default function ContextViewer() {
return (
<div style={containerStyle}>
<p style={descStyle}>
Top 10 most-connected nodes are added to background context for tool execution.
Context summaries and anchor nodes are used first. Global hub nodes remain secondary diagnostics in background context.
</p>
{/* Toggle */}
+21 -5
View File
@@ -3,6 +3,7 @@
import { Node } from '@/types/database';
import { getNodeIcon } from '@/utils/nodeIcons';
import { useDimensionIcons } from '@/context/DimensionIconsContext';
import { getNodeProcessedState } from '@/services/nodes/metadata';
interface GridViewProps {
nodes: Node[];
@@ -43,7 +44,11 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
gap: '12px'
}}>
{nodes.map(node => (
{nodes.map(node => {
const processedState = getNodeProcessedState(node.metadata);
const isProcessed = processedState === 'processed';
return (
<button
key={node.id}
onClick={() => onNodeClick(node.id)}
@@ -51,13 +56,14 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
display: 'flex',
flexDirection: 'column',
padding: '16px',
background: '#0a0a0a',
border: '1px solid #1a1a1a',
background: isProcessed ? 'rgba(34, 58, 42, 0.45)' : '#0a0a0a',
border: `1px solid ${isProcessed ? 'rgba(74, 222, 128, 0.28)' : '#1a1a1a'}`,
borderRadius: '8px',
cursor: 'pointer',
textAlign: 'left',
transition: 'all 0.2s',
minHeight: '140px'
minHeight: '140px',
opacity: isProcessed ? 0.84 : 1
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#111';
@@ -103,6 +109,15 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
</div>
</div>
<div style={{
marginBottom: '10px',
fontSize: '10px',
color: isProcessed ? '#86efac' : '#888',
textTransform: 'lowercase'
}}>
{processedState}
</div>
{/* Description or Content Preview */}
{(node.description || node.source) && (
<div style={{
@@ -154,7 +169,8 @@ export default function GridView({ nodes, onNodeClick }: GridViewProps) {
</div>
)}
</button>
))}
);
})}
</div>
</div>
);
+22 -5
View File
@@ -5,6 +5,7 @@ import { Node } from '@/types/database';
import { getNodeIcon } from '@/utils/nodeIcons';
import { useDimensionIcons } from '@/context/DimensionIconsContext';
import { formatRelativeDate } from '@/utils/formatDate';
import { getNodeProcessedState } from '@/services/nodes/metadata';
interface ListViewProps {
nodes: Node[];
@@ -48,7 +49,11 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
overflowY: 'auto',
padding: '8px'
}}>
{nodes.map(node => (
{nodes.map(node => {
const processedState = getNodeProcessedState(node.metadata);
const isProcessed = processedState === 'processed';
return (
<button
key={node.id}
onClick={() => onNodeClick(node.id)}
@@ -59,13 +64,14 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
gap: '12px',
padding: '12px',
marginBottom: '4px',
background: 'var(--rah-bg-base)',
background: isProcessed ? 'var(--rah-bg-subtle, var(--rah-bg-base))' : 'var(--rah-bg-base)',
border: '1px solid var(--rah-border)',
borderLeft: '2px solid var(--rah-border-stronger)',
borderLeft: `2px solid ${isProcessed ? 'var(--rah-accent-green, #4ade80)' : 'var(--rah-border-stronger)'}`,
borderRadius: '6px',
cursor: 'pointer',
textAlign: 'left',
transition: 'all 0.15s ease'
transition: 'all 0.15s ease',
opacity: isProcessed ? 0.82 : 1
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--rah-bg-surface)';
@@ -132,6 +138,16 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
gap: '12px',
flexWrap: 'wrap'
}}>
<span style={{
padding: '2px 8px',
background: isProcessed ? 'rgba(74, 222, 128, 0.10)' : 'var(--rah-bg-active)',
border: `1px solid ${isProcessed ? 'rgba(74, 222, 128, 0.35)' : 'var(--rah-border-strong)'}`,
borderRadius: '999px',
fontSize: '11px',
color: isProcessed ? 'var(--rah-accent-green, #4ade80)' : 'var(--rah-text-muted)'
}}>
{processedState}
</span>
{/* Dimensions */}
{node.dimensions && node.dimensions.length > 0 && (
<div style={{
@@ -186,7 +202,8 @@ export default function ListView({ nodes, onNodeClick }: ListViewProps) {
</div>
</div>
</button>
))}
);
})}
</div>
);
}
File diff suppressed because it is too large Load Diff