feat: port contexts layer and MCP parity
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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}`}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)',
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user