sync: features from private repo (2025-12-24)
Synced from ra-h private repo: - Collapsible chat panel (Cmd+\) - FolderViewOverlay with Kanban/Grid/List views - Updated agent prompts - Improved executor and quickAdd services - New views.ts types Files kept at OS versions (auth/backend deps): - SettingsModal, RAHChat, useSSEChat - Delegation tools and wiseRAHExecutor - openExternalUrl (no Tauri) Added stub: supabaseTokenRegistry.ts for compatibility
This commit is contained in:
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import RAHChat from './RAHChat';
|
import RAHChat from './RAHChat';
|
||||||
import QuickAddInput from './QuickAddInput';
|
import QuickAddInput from './QuickAddInput';
|
||||||
import QuickAddStatus from './QuickAddStatus';
|
import QuickAddStatus from './QuickAddStatus';
|
||||||
import { Zap, Flame } from 'lucide-react';
|
import { Zap, Flame, Minimize2 } from 'lucide-react';
|
||||||
import type { AgentDelegation } from '@/services/agents/delegation';
|
import type { AgentDelegation } from '@/services/agents/delegation';
|
||||||
import { Node } from '@/types/database';
|
import { Node } from '@/types/database';
|
||||||
|
|
||||||
@@ -12,12 +12,13 @@ interface AgentsPanelProps {
|
|||||||
openTabsData: Node[];
|
openTabsData: Node[];
|
||||||
activeTabId: number | null;
|
activeTabId: number | null;
|
||||||
onNodeClick?: (nodeId: number) => void;
|
onNodeClick?: (nodeId: number) => void;
|
||||||
|
onCollapse?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActiveTab = 'ra-h' | string; // 'ra-h' or delegation sessionId
|
type ActiveTab = 'ra-h' | string; // 'ra-h' or delegation sessionId
|
||||||
type Mode = 'quickadd' | 'session';
|
type Mode = 'quickadd' | 'session';
|
||||||
|
|
||||||
export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick }: AgentsPanelProps) {
|
export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, onCollapse }: AgentsPanelProps) {
|
||||||
const [delegationsMap, setDelegationsMap] = useState<Record<string, AgentDelegation>>({});
|
const [delegationsMap, setDelegationsMap] = useState<Record<string, AgentDelegation>>({});
|
||||||
const [activeAgentTab, setActiveAgentTab] = useState<ActiveTab>('ra-h');
|
const [activeAgentTab, setActiveAgentTab] = useState<ActiveTab>('ra-h');
|
||||||
const [mode, setMode] = useState<Mode>('quickadd');
|
const [mode, setMode] = useState<Mode>('quickadd');
|
||||||
@@ -186,7 +187,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick }:
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#0a0a0a' }}>
|
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#0a0a0a', position: 'relative' }}>
|
||||||
{/* Mode Header */}
|
{/* Mode Header */}
|
||||||
{mode === 'quickadd' ? (
|
{mode === 'quickadd' ? (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -198,8 +199,42 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick }:
|
|||||||
gap: '24px',
|
gap: '24px',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
height: '100%'
|
height: '100%',
|
||||||
|
position: 'relative'
|
||||||
}}>
|
}}>
|
||||||
|
{/* Collapse button - top right */}
|
||||||
|
{onCollapse && (
|
||||||
|
<button
|
||||||
|
onClick={onCollapse}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '12px',
|
||||||
|
left: '12px',
|
||||||
|
width: '28px',
|
||||||
|
height: '28px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid #1f1f1f',
|
||||||
|
background: 'transparent',
|
||||||
|
color: '#666',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.15s ease'
|
||||||
|
}}
|
||||||
|
title="Collapse chat panel (⌘\)"
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#1a1a1a';
|
||||||
|
e.currentTarget.style.color = '#999';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'transparent';
|
||||||
|
e.currentTarget.style.color = '#666';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Minimize2 size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* Top spacer */}
|
{/* Top spacer */}
|
||||||
<div style={{ flex: 1 }} />
|
<div style={{ flex: 1 }} />
|
||||||
|
|
||||||
@@ -307,7 +342,39 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick }:
|
|||||||
|
|
||||||
{/* Tab Bar (only show in session mode) */}
|
{/* Tab Bar (only show in session mode) */}
|
||||||
{mode === 'session' && (
|
{mode === 'session' && (
|
||||||
<div className="agent-tabs" style={{ position: 'relative' }}>
|
<div className="agent-tabs" style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
|
||||||
|
{/* Collapse button - first item */}
|
||||||
|
{onCollapse && (
|
||||||
|
<button
|
||||||
|
onClick={onCollapse}
|
||||||
|
style={{
|
||||||
|
width: '28px',
|
||||||
|
height: '28px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid #1f1f1f',
|
||||||
|
background: 'transparent',
|
||||||
|
color: '#666',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
marginRight: '8px',
|
||||||
|
flexShrink: 0
|
||||||
|
}}
|
||||||
|
title="Collapse chat panel (⌘\)"
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#1a1a1a';
|
||||||
|
e.currentTarget.style.color = '#999';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'transparent';
|
||||||
|
e.currentTarget.style.color = '#666';
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Minimize2 size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{/* Quick Add button - positioned at far right */}
|
{/* Quick Add button - positioned at far right */}
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Node } from '@/types/database';
|
|||||||
import { DatabaseEvent } from '@/services/events';
|
import { DatabaseEvent } from '@/services/events';
|
||||||
import { usePersistentState } from '@/hooks/usePersistentState';
|
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||||
import FolderViewOverlay from '../nodes/FolderViewOverlay';
|
import FolderViewOverlay from '../nodes/FolderViewOverlay';
|
||||||
|
import { Maximize2 } from 'lucide-react';
|
||||||
|
|
||||||
export default function ThreePanelLayout() {
|
export default function ThreePanelLayout() {
|
||||||
// Panel widths as percentages (20% | 40% | 40%)
|
// Panel widths as percentages (20% | 40% | 40%)
|
||||||
@@ -19,6 +20,9 @@ export default function ThreePanelLayout() {
|
|||||||
// Collapsible state for nodes panel
|
// Collapsible state for nodes panel
|
||||||
const [nodesCollapsed, setNodesCollapsed] = usePersistentState('ui.nodesCollapsed', false);
|
const [nodesCollapsed, setNodesCollapsed] = usePersistentState('ui.nodesCollapsed', false);
|
||||||
|
|
||||||
|
// Collapsible state for chat/agents panel
|
||||||
|
const [chatCollapsed, setChatCollapsed] = usePersistentState('ui.chatCollapsed', false);
|
||||||
|
|
||||||
// Settings dropdown state
|
// Settings dropdown state
|
||||||
const [showSettings, setShowSettings] = useState(false);
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const [settingsInitialTab, setSettingsInitialTab] = useState<SettingsTab>();
|
const [settingsInitialTab, setSettingsInitialTab] = useState<SettingsTab>();
|
||||||
@@ -89,26 +93,21 @@ export default function ThreePanelLayout() {
|
|||||||
// Keyboard shortcut handler
|
// Keyboard shortcut handler
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
// Check for Cmd+1 (Mac) or Ctrl+1 (Windows/Linux)
|
// Check for Cmd+1 (Mac) or Ctrl+1 (Windows/Linux) - toggle nodes panel
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === '1') {
|
if ((e.metaKey || e.ctrlKey) && e.key === '1') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setNodesCollapsed(prev => !prev);
|
setNodesCollapsed(prev => !prev);
|
||||||
}
|
}
|
||||||
|
// Check for Cmd+\ (Mac) or Ctrl+\ (Windows/Linux) - toggle chat panel
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key === '\\') {
|
||||||
|
e.preventDefault();
|
||||||
|
setChatCollapsed(prev => !prev);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [setNodesCollapsed]);
|
}, [setNodesCollapsed, setChatCollapsed]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleSettingsOpen = (event: Event) => {
|
|
||||||
const detail = (event as CustomEvent<{ tab?: SettingsTab }>).detail;
|
|
||||||
setSettingsInitialTab(detail?.tab);
|
|
||||||
setShowSettings(true);
|
|
||||||
};
|
|
||||||
window.addEventListener('settings:open', handleSettingsOpen as EventListener);
|
|
||||||
return () => window.removeEventListener('settings:open', handleSettingsOpen as EventListener);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
|
||||||
// SSE connection for real-time updates
|
// SSE connection for real-time updates
|
||||||
@@ -373,9 +372,16 @@ export default function ThreePanelLayout() {
|
|||||||
handleCloseTab(nodeId);
|
handleCloseTab(nodeId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const rightWidth = nodesCollapsed ? (100 - middleWidth) : (100 - leftWidth - middleWidth);
|
// Calculate panel widths based on collapse states
|
||||||
|
const baseRightWidth = nodesCollapsed ? (100 - middleWidth) : (100 - leftWidth - middleWidth);
|
||||||
const effectiveLeftWidth = nodesCollapsed ? 0 : leftWidth;
|
const effectiveLeftWidth = nodesCollapsed ? 0 : leftWidth;
|
||||||
const effectiveMiddleWidth = nodesCollapsed ? (leftWidth + middleWidth) : middleWidth;
|
|
||||||
|
// When chat is collapsed, middle panel takes its space (but leave room for 64px rail)
|
||||||
|
const effectiveRightWidth = chatCollapsed ? 0 : baseRightWidth;
|
||||||
|
// Note: When chatCollapsed, we use calc() to leave room for the 64px collapsed rail
|
||||||
|
const effectiveMiddleWidth = chatCollapsed
|
||||||
|
? (nodesCollapsed ? 100 : (middleWidth + baseRightWidth))
|
||||||
|
: (nodesCollapsed ? (leftWidth + middleWidth) : middleWidth);
|
||||||
|
|
||||||
const activeNodeId = activeTab;
|
const activeNodeId = activeTab;
|
||||||
|
|
||||||
@@ -478,9 +484,13 @@ export default function ThreePanelLayout() {
|
|||||||
{/* Middle Panel - Focus */}
|
{/* Middle Panel - Focus */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: `${effectiveMiddleWidth}%`,
|
width: chatCollapsed
|
||||||
|
? (nodesCollapsed
|
||||||
|
? `calc(100% - 48px - 40px)`
|
||||||
|
: `calc(${effectiveMiddleWidth}% - 48px)`)
|
||||||
|
: `${effectiveMiddleWidth}%`,
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
borderRight: '1px solid #1a1a1a',
|
borderRight: chatCollapsed ? 'none' : '1px solid #1a1a1a',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -516,7 +526,7 @@ export default function ThreePanelLayout() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Resize Handle */}
|
{/* Right Resize Handle */}
|
||||||
{!nodesCollapsed && (
|
{!nodesCollapsed && !chatCollapsed && (
|
||||||
<div
|
<div
|
||||||
onMouseDown={() => setIsDraggingRight(true)}
|
onMouseDown={() => setIsDraggingRight(true)}
|
||||||
style={{
|
style={{
|
||||||
@@ -530,25 +540,68 @@ export default function ThreePanelLayout() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Right Panel - Agents */}
|
{/* Right Panel - Agents (collapsible) */}
|
||||||
<div
|
{chatCollapsed ? (
|
||||||
style={{
|
// Collapsed rail
|
||||||
width: `${rightWidth}%`,
|
<div style={{
|
||||||
|
width: '48px',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
overflow: 'hidden',
|
borderLeft: '1px solid #2a2a2a',
|
||||||
|
background: '#0f0f0f',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
background: '#0a0a0a',
|
alignItems: 'center',
|
||||||
position: 'relative',
|
paddingTop: '12px'
|
||||||
padding: '4px'
|
}}>
|
||||||
}}
|
<button
|
||||||
>
|
onClick={() => setChatCollapsed(false)}
|
||||||
<AgentsPanel
|
style={{
|
||||||
openTabsData={openTabsData}
|
width: '28px',
|
||||||
activeTabId={activeNodeId}
|
height: '28px',
|
||||||
onNodeClick={(nodeId) => handleNodeSelect(nodeId, false)}
|
borderRadius: '6px',
|
||||||
/>
|
border: '1px solid #1f1f1f',
|
||||||
</div>
|
background: 'transparent',
|
||||||
|
color: '#666',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.15s ease'
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.background = '#1a1a1a';
|
||||||
|
e.currentTarget.style.color = '#999';
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.background = 'transparent';
|
||||||
|
e.currentTarget.style.color = '#666';
|
||||||
|
}}
|
||||||
|
title="Expand Chat (⌘\)"
|
||||||
|
>
|
||||||
|
<Maximize2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: `${effectiveRightWidth}%`,
|
||||||
|
flexShrink: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
background: '#0a0a0a',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AgentsPanel
|
||||||
|
openTabsData={openTabsData}
|
||||||
|
activeTabId={activeNodeId}
|
||||||
|
onNodeClick={(nodeId) => handleNodeSelect(nodeId, false)}
|
||||||
|
onCollapse={() => setChatCollapsed(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Settings Modal */}
|
{/* Settings Modal */}
|
||||||
<SettingsModal
|
<SettingsModal
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, type DragEvent } from 'react';
|
import { useState, useEffect, useRef, type DragEvent } from 'react';
|
||||||
import { ChevronRight, ChevronDown, Folder, FolderOpen, Layers, List, Maximize2, Minimize2 } from 'lucide-react';
|
import { ChevronRight, ChevronDown, Folder, FolderOpen, Layers, List, Maximize2, Minimize2 } from 'lucide-react';
|
||||||
import { Node } from '@/types/database';
|
import { Node } from '@/types/database';
|
||||||
import AddNodeButton from './AddNodeButton';
|
import AddNodeButton from './AddNodeButton';
|
||||||
@@ -45,6 +45,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
|||||||
const [showSearchModal, setShowSearchModal] = useState(false);
|
const [showSearchModal, setShowSearchModal] = useState(false);
|
||||||
const [dropFeedback, setDropFeedback] = useState<string | null>(null);
|
const [dropFeedback, setDropFeedback] = useState<string | null>(null);
|
||||||
const [dragHoverDimension, setDragHoverDimension] = useState<string | null>(null);
|
const [dragHoverDimension, setDragHoverDimension] = useState<string | null>(null);
|
||||||
|
const draggedNodeRef = useRef<{ id: number; dimensions?: string[] } | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchNodes();
|
fetchNodes();
|
||||||
@@ -153,14 +154,14 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDimensionDragOver = (event: DragEvent<HTMLElement>) => {
|
const handleDimensionDragOver = (event: DragEvent<HTMLElement>) => {
|
||||||
if (event.dataTransfer.types.includes('application/node-info')) {
|
if (event.dataTransfer.types.includes('application/node-info') || event.dataTransfer.types.includes('text/plain')) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer.dropEffect = 'copy';
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDimensionDragEnter = (event: DragEvent<HTMLElement>, dimension: string) => {
|
const handleDimensionDragEnter = (event: DragEvent<HTMLElement>, dimension: string) => {
|
||||||
if (event.dataTransfer.types.includes('application/node-info')) {
|
if (event.dataTransfer.types.includes('application/node-info') || event.dataTransfer.types.includes('text/plain')) {
|
||||||
setDragHoverDimension(dimension);
|
setDragHoverDimension(dimension);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -172,17 +173,33 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleNodeDropOnDimension = async (event: DragEvent<HTMLElement>, dimension: string) => {
|
const handleNodeDropOnDimension = async (event: DragEvent<HTMLElement>, dimension: string) => {
|
||||||
if (!event.dataTransfer.types.includes('application/node-info')) {
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
// Try to get data from ref first (works in Electron/Tauri webviews)
|
||||||
|
let payload: { id: number; dimensions?: string[] } | null = draggedNodeRef.current;
|
||||||
|
|
||||||
|
// Fallback to dataTransfer for browser compatibility
|
||||||
|
if (!payload) {
|
||||||
|
const raw = event.dataTransfer.getData('application/node-info') || event.dataTransfer.getData('text/plain');
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse drag data:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the ref
|
||||||
|
draggedNodeRef.current = null;
|
||||||
|
|
||||||
|
if (!payload?.id) {
|
||||||
|
console.warn('No valid node data in drop event');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
|
||||||
const raw = event.dataTransfer.getData('application/node-info');
|
|
||||||
if (!raw) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(raw) as { id: number; dimensions?: string[] };
|
|
||||||
if (!payload?.id) return;
|
|
||||||
|
|
||||||
const currentDimensions = payload.dimensions || [];
|
const currentDimensions = payload.dimensions || [];
|
||||||
if (currentDimensions.some((dim) => dim.toLowerCase() === dimension.toLowerCase())) {
|
if (currentDimensions.some((dim) => dim.toLowerCase() === dimension.toLowerCase())) {
|
||||||
setDropFeedback(`Node already in ${dimension}`);
|
setDropFeedback(`Node already in ${dimension}`);
|
||||||
@@ -368,10 +385,15 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
|||||||
|
|
||||||
const handleNodeDragStart = (event: DragEvent<HTMLElement>, node: Node) => {
|
const handleNodeDragStart = (event: DragEvent<HTMLElement>, node: Node) => {
|
||||||
event.dataTransfer.effectAllowed = 'copy';
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
event.dataTransfer.setData('application/node-info', JSON.stringify({
|
const nodeData = {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
dimensions: node.dimensions || []
|
dimensions: node.dimensions || []
|
||||||
}));
|
};
|
||||||
|
// Store in ref for webview compatibility (dataTransfer.getData can fail in Electron/Tauri)
|
||||||
|
draggedNodeRef.current = nodeData;
|
||||||
|
// Also set in dataTransfer for browser compatibility
|
||||||
|
event.dataTransfer.setData('application/node-info', JSON.stringify(nodeData));
|
||||||
|
event.dataTransfer.setData('text/plain', JSON.stringify(nodeData));
|
||||||
|
|
||||||
const preview = document.createElement('div');
|
const preview = document.createElement('div');
|
||||||
preview.textContent = node.title || `Node #${node.id}`;
|
preview.textContent = node.title || `Node #${node.id}`;
|
||||||
@@ -394,11 +416,17 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
|||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNodeDragEnd = () => {
|
||||||
|
// Clear ref if drag ends without a drop
|
||||||
|
draggedNodeRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
const renderNodeItem = (node: Node) => (
|
const renderNodeItem = (node: Node) => (
|
||||||
<button
|
<button
|
||||||
key={node.id}
|
key={node.id}
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(event) => handleNodeDragStart(event, node)}
|
onDragStart={(event) => handleNodeDragStart(event, node)}
|
||||||
|
onDragEnd={handleNodeDragEnd}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
const multiSelect = e.metaKey || e.ctrlKey;
|
const multiSelect = e.metaKey || e.ctrlKey;
|
||||||
onNodeSelect(node.id, multiSelect);
|
onNodeSelect(node.id, multiSelect);
|
||||||
@@ -580,27 +608,29 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
|||||||
<button
|
<button
|
||||||
onClick={() => onToggleFolderView?.()}
|
onClick={() => onToggleFolderView?.()}
|
||||||
style={{
|
style={{
|
||||||
width: '48px',
|
width: '28px',
|
||||||
height: '48px',
|
height: '28px',
|
||||||
borderRadius: '10px',
|
borderRadius: '6px',
|
||||||
border: '1px solid #1f1f1f',
|
border: '1px solid #1f1f1f',
|
||||||
background: folderViewOpen ? '#16301f' : '#080808',
|
background: 'transparent',
|
||||||
color: folderViewOpen ? '#22c55e' : '#cbd5f5',
|
color: '#666',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'background 0.2s ease, color 0.2s ease'
|
transition: 'all 0.15s ease'
|
||||||
}}
|
}}
|
||||||
title={folderViewOpen ? 'Close dimension folder view' : 'Open dimension folder view'}
|
title={folderViewOpen ? 'Close dimension folder view' : 'Open dimension folder view'}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = folderViewOpen ? '#1c3b27' : '#101010';
|
e.currentTarget.style.background = '#1a1a1a';
|
||||||
|
e.currentTarget.style.color = '#999';
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.background = folderViewOpen ? '#16301f' : '#080808';
|
e.currentTarget.style.background = 'transparent';
|
||||||
|
e.currentTarget.style.color = '#666';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{folderViewOpen ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
|
{folderViewOpen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ Tool strategy:
|
|||||||
- youtubeExtract, websiteExtract, and paperExtract when outside content is required.
|
- youtubeExtract, websiteExtract, and paperExtract when outside content is required.
|
||||||
- webSearch only when the knowledge base lacks the answer.
|
- webSearch only when the knowledge base lacks the answer.
|
||||||
|
|
||||||
|
Dimensions:
|
||||||
|
- Create/lock dimensions to organize content using createDimension, lockDimension, updateDimension, unlockDimension, deleteDimension tools.
|
||||||
|
- Lock dimensions (isPriority=true) so they auto-assign to new nodes.
|
||||||
|
|
||||||
Response polish:
|
Response polish:
|
||||||
- Default to minimal reasoning effort for speed.
|
- Default to minimal reasoning effort for speed.
|
||||||
- Do not expose chain-of-thought; return conclusions only.
|
- Do not expose chain-of-thought; return conclusions only.
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ Tool strategy:
|
|||||||
- Extract content with youtubeExtract, websiteExtract, paperExtract as needed.
|
- Extract content with youtubeExtract, websiteExtract, paperExtract as needed.
|
||||||
- When searchContentEmbeddings highlights a chunk, hydrate the node via getNodesById (or fetch the chunk) before quoting.
|
- When searchContentEmbeddings highlights a chunk, hydrate the node via getNodesById (or fetch the chunk) before quoting.
|
||||||
|
|
||||||
|
Dimension management:
|
||||||
|
- Create dimensions for new knowledge areas or topics using createDimension.
|
||||||
|
- Lock dimensions (isPriority=true) to enable auto-assignment to new nodes.
|
||||||
|
- Update descriptions to help the AI understand dimension purpose.
|
||||||
|
- Use lockDimension/unlockDimension for quick lock status changes.
|
||||||
|
- Delete unused dimensions to keep the system clean.
|
||||||
|
|
||||||
Response style:
|
Response style:
|
||||||
- Limit to one or two short sentences. Reference nodes as [NODE:id:"title"].
|
- Limit to one or two short sentences. Reference nodes as [NODE:id:"title"].
|
||||||
- When answering about stored content, quote the exact wording from the chunk (verbatim, in quotation marks) and cite the node.
|
- When answering about stored content, quote the exact wording from the chunk (verbatim, in quotation marks) and cite the node.
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ Additional guidance:
|
|||||||
- If you cannot complete a step (missing node, empty content, tool failure), state that explicitly in 'Follow-up' with the precise next action needed.
|
- If you cannot complete a step (missing node, empty content, tool failure), state that explicitly in 'Follow-up' with the precise next action needed.
|
||||||
- Stop after success—do not run extra verification tools.
|
- Stop after success—do not run extra verification tools.
|
||||||
- Keep the full summary under ~100 tokens.
|
- Keep the full summary under ~100 tokens.
|
||||||
|
- You can create and manage dimensions using createDimension, updateDimension, lockDimension, unlockDimension, deleteDimension tools. Lock dimensions (isPriority=true) to enable auto-assignment.
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Available tools:
|
|||||||
- webSearch — external research when necessary
|
- webSearch — external research when necessary
|
||||||
- think — internal planning/reflection (use once per workflow unless plan changes)
|
- think — internal planning/reflection (use once per workflow unless plan changes)
|
||||||
- updateNode — append content to nodes (tool handles appending automatically)
|
- updateNode — append content to nodes (tool handles appending automatically)
|
||||||
|
- Dimension management: createDimension, updateDimension, lockDimension, unlockDimension, deleteDimension — manage dimensions to organize knowledge base structure
|
||||||
</tools>
|
</tools>
|
||||||
|
|
||||||
<execution>
|
<execution>
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { calculateCost } from '@/services/analytics/pricing';
|
|||||||
import { UsageData } from '@/types/analytics';
|
import { UsageData } from '@/types/analytics';
|
||||||
import { summarizeToolExecution } from '@/services/agents/toolResultUtils';
|
import { summarizeToolExecution } from '@/services/agents/toolResultUtils';
|
||||||
import { edgeService } from '@/services/database/edges';
|
import { edgeService } from '@/services/database/edges';
|
||||||
import { RequestContext } from '@/services/context/requestContext';
|
|
||||||
|
|
||||||
interface CapsuleNodeJSON {
|
interface CapsuleNodeJSON {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -121,11 +120,7 @@ export interface MiniRAHExecutionInput {
|
|||||||
export class MiniRAHExecutor {
|
export class MiniRAHExecutor {
|
||||||
static async execute({ sessionId, task, context, expectedOutcome, traceId, parentChatId, workflowKey, workflowNodeId }: MiniRAHExecutionInput) {
|
static async execute({ sessionId, task, context, expectedOutcome, traceId, parentChatId, workflowKey, workflowNodeId }: MiniRAHExecutionInput) {
|
||||||
try {
|
try {
|
||||||
const requestContext = RequestContext.get();
|
const delegateKey = process.env.RAH_DELEGATE_OPENAI_API_KEY || process.env.OPENAI_API_KEY;
|
||||||
const delegateKey =
|
|
||||||
requestContext.apiKeys?.openai ||
|
|
||||||
process.env.RAH_DELEGATE_OPENAI_API_KEY ||
|
|
||||||
process.env.OPENAI_API_KEY;
|
|
||||||
if (!delegateKey) {
|
if (!delegateKey) {
|
||||||
throw new Error('RAH_DELEGATE_OPENAI_API_KEY (or OPENAI_API_KEY) is not set.');
|
throw new Error('RAH_DELEGATE_OPENAI_API_KEY (or OPENAI_API_KEY) is not set.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ async function handleNoteQuickAdd(rawInput: string, task: string): Promise<strin
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
dimensions: [],
|
|
||||||
metadata: {
|
metadata: {
|
||||||
source: 'quick-add-note',
|
source: 'quick-add-note',
|
||||||
refined_at: new Date().toISOString(),
|
refined_at: new Date().toISOString(),
|
||||||
@@ -307,7 +306,6 @@ async function handleChatTranscriptQuickAdd(rawInput: string, task: string): Pro
|
|||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
chunk: transcript,
|
chunk: transcript,
|
||||||
dimensions: [],
|
|
||||||
metadata,
|
metadata,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Stub for SupabaseTokenRegistry - not used in open source version
|
||||||
|
* The private version uses this for backend API authentication.
|
||||||
|
* In open source mode, all API calls go directly to OpenAI/Anthropic.
|
||||||
|
*/
|
||||||
|
export class SupabaseTokenRegistry {
|
||||||
|
static async get(_key: string): Promise<string | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getLast(_key: string): string | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static set(_key: string, _value: string): void {
|
||||||
|
// No-op in OS version
|
||||||
|
}
|
||||||
|
|
||||||
|
static delete(_key: string): void {
|
||||||
|
// No-op in OS version
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ export interface ContextBuilderOptions {
|
|||||||
const BASE_CONTEXT = `=== RA-H BASE CONTEXT ===
|
const BASE_CONTEXT = `=== RA-H BASE CONTEXT ===
|
||||||
- Nodes store content (title, content, dimensions, metadata, link, chunk)
|
- Nodes store content (title, content, dimensions, metadata, link, chunk)
|
||||||
- Edges capture directed relationships between nodes
|
- Edges capture directed relationships between nodes
|
||||||
|
- Dimensions organize nodes; locked dimensions (isPriority=true) auto-assign to new nodes
|
||||||
|
- You can create and manage dimensions using dimension tools (createDimension, updateDimension, lockDimension, unlockDimension, deleteDimension)
|
||||||
- When auto-context is enabled you'll see BACKGROUND CONTEXT with the 10 most-connected nodes (ID + title only)
|
- When auto-context is enabled you'll see BACKGROUND CONTEXT with the 10 most-connected nodes (ID + title only)
|
||||||
- Focused nodes show truncated content; use queryNodes, searchContentEmbeddings, or queryEdge when you need full detail
|
- Focused nodes show truncated content; use queryNodes, searchContentEmbeddings, or queryEdge when you need full detail
|
||||||
- Node references must use [NODE:id:"title"] so the UI renders clickable labels
|
- Node references must use [NODE:id:"title"] so the UI renders clickable labels
|
||||||
|
|||||||
@@ -32,12 +32,6 @@ export class ApiKeyService {
|
|||||||
this.loadKeys();
|
this.loadKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
private notifyUpdate(): void {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.dispatchEvent(new CustomEvent('api-keys:updated'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load keys from localStorage
|
// Load keys from localStorage
|
||||||
private loadKeys(): void {
|
private loadKeys(): void {
|
||||||
try {
|
try {
|
||||||
@@ -81,7 +75,6 @@ export class ApiKeyService {
|
|||||||
if (this.validateOpenAiKey(key)) {
|
if (this.validateOpenAiKey(key)) {
|
||||||
this.keys.openai = key;
|
this.keys.openai = key;
|
||||||
this.saveKeys();
|
this.saveKeys();
|
||||||
this.notifyUpdate();
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid OpenAI API key format');
|
throw new Error('Invalid OpenAI API key format');
|
||||||
}
|
}
|
||||||
@@ -92,7 +85,6 @@ export class ApiKeyService {
|
|||||||
if (this.validateAnthropicKey(key)) {
|
if (this.validateAnthropicKey(key)) {
|
||||||
this.keys.anthropic = key;
|
this.keys.anthropic = key;
|
||||||
this.saveKeys();
|
this.saveKeys();
|
||||||
this.notifyUpdate();
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Invalid Anthropic API key format');
|
throw new Error('Invalid Anthropic API key format');
|
||||||
}
|
}
|
||||||
@@ -103,14 +95,12 @@ export class ApiKeyService {
|
|||||||
delete this.keys.openai;
|
delete this.keys.openai;
|
||||||
this.saveKeys();
|
this.saveKeys();
|
||||||
this.status.openai = 'not-set';
|
this.status.openai = 'not-set';
|
||||||
this.notifyUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clearAnthropicKey(): void {
|
clearAnthropicKey(): void {
|
||||||
delete this.keys.anthropic;
|
delete this.keys.anthropic;
|
||||||
this.saveKeys();
|
this.saveKeys();
|
||||||
this.status.anthropic = 'not-set';
|
this.status.anthropic = 'not-set';
|
||||||
this.notifyUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all keys
|
// Clear all keys
|
||||||
@@ -121,7 +111,6 @@ export class ApiKeyService {
|
|||||||
openai: 'not-set',
|
openai: 'not-set',
|
||||||
anthropic: 'not-set'
|
anthropic: 'not-set'
|
||||||
};
|
};
|
||||||
this.notifyUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get masked key for display (show only last 4 characters)
|
// Get masked key for display (show only last 4 characters)
|
||||||
@@ -184,16 +173,26 @@ export class ApiKeyService {
|
|||||||
async testAnthropicConnection(key?: string): Promise<boolean> {
|
async testAnthropicConnection(key?: string): Promise<boolean> {
|
||||||
const testKey = key || this.getAnthropicKey();
|
const testKey = key || this.getAnthropicKey();
|
||||||
if (!testKey) return false;
|
if (!testKey) return false;
|
||||||
|
|
||||||
this.status.anthropic = 'testing';
|
this.status.anthropic = 'testing';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/local/test-anthropic', {
|
// Simple test call to Anthropic API
|
||||||
|
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
body: JSON.stringify({ apiKey: testKey }),
|
'x-api-key': testKey,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'anthropic-version': '2023-06-01'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: 'claude-3-haiku-20240307',
|
||||||
|
max_tokens: 1,
|
||||||
|
messages: [{ role: 'user', content: 'test' }]
|
||||||
|
})
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
|
||||||
const isConnected = Boolean(data?.ok);
|
const isConnected = response.ok;
|
||||||
this.status.anthropic = isConnected ? 'connected' : 'failed';
|
this.status.anthropic = isConnected ? 'connected' : 'failed';
|
||||||
return isConnected;
|
return isConnected;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -171,3 +171,11 @@ export interface DatabaseError {
|
|||||||
code?: string;
|
code?: string;
|
||||||
details?: any;
|
details?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dimension interface for dimension management
|
||||||
|
export interface Dimension {
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
is_priority: boolean;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// View system types
|
||||||
|
|
||||||
|
export type ViewType = 'focus' | 'list' | 'kanban' | 'grid';
|
||||||
|
|
||||||
|
export interface ViewFilter {
|
||||||
|
dimension: string;
|
||||||
|
operator: 'includes' | 'excludes';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewSort {
|
||||||
|
field: 'title' | 'created_at' | 'updated_at' | 'edge_count';
|
||||||
|
direction: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KanbanColumn {
|
||||||
|
id: string;
|
||||||
|
dimension: string;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewConfig {
|
||||||
|
filters: ViewFilter[];
|
||||||
|
filterLogic: 'and' | 'or';
|
||||||
|
sort: ViewSort;
|
||||||
|
// Kanban-specific
|
||||||
|
columns?: KanbanColumn[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedView {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: ViewType;
|
||||||
|
config: ViewConfig;
|
||||||
|
is_default: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default view configuration
|
||||||
|
export const DEFAULT_VIEW_CONFIG: ViewConfig = {
|
||||||
|
filters: [],
|
||||||
|
filterLogic: 'and',
|
||||||
|
sort: { field: 'updated_at', direction: 'desc' },
|
||||||
|
columns: []
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user