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 QuickAddInput from './QuickAddInput';
|
||||
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 { Node } from '@/types/database';
|
||||
|
||||
@@ -12,12 +12,13 @@ interface AgentsPanelProps {
|
||||
openTabsData: Node[];
|
||||
activeTabId: number | null;
|
||||
onNodeClick?: (nodeId: number) => void;
|
||||
onCollapse?: () => void;
|
||||
}
|
||||
|
||||
type ActiveTab = 'ra-h' | string; // 'ra-h' or delegation sessionId
|
||||
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 [activeAgentTab, setActiveAgentTab] = useState<ActiveTab>('ra-h');
|
||||
const [mode, setMode] = useState<Mode>('quickadd');
|
||||
@@ -186,7 +187,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick }:
|
||||
};
|
||||
|
||||
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 === 'quickadd' ? (
|
||||
<div style={{
|
||||
@@ -198,8 +199,42 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick }:
|
||||
gap: '24px',
|
||||
justifyContent: 'space-between',
|
||||
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 */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
@@ -307,7 +342,39 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick }:
|
||||
|
||||
{/* Tab Bar (only show in session mode) */}
|
||||
{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 */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
|
||||
@@ -1040,7 +1040,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
<>
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#000' }}>
|
||||
{/* Tab Bar */}
|
||||
<div style={{
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
borderBottom: '1px solid #333',
|
||||
background: '#0a0a0a',
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Node } from '@/types/database';
|
||||
import { DatabaseEvent } from '@/services/events';
|
||||
import { usePersistentState } from '@/hooks/usePersistentState';
|
||||
import FolderViewOverlay from '../nodes/FolderViewOverlay';
|
||||
import { Maximize2 } from 'lucide-react';
|
||||
|
||||
export default function ThreePanelLayout() {
|
||||
// Panel widths as percentages (20% | 40% | 40%)
|
||||
@@ -18,6 +19,9 @@ export default function ThreePanelLayout() {
|
||||
|
||||
// Collapsible state for nodes panel
|
||||
const [nodesCollapsed, setNodesCollapsed] = usePersistentState('ui.nodesCollapsed', false);
|
||||
|
||||
// Collapsible state for chat/agents panel
|
||||
const [chatCollapsed, setChatCollapsed] = usePersistentState('ui.chatCollapsed', false);
|
||||
|
||||
// Settings dropdown state
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
@@ -89,26 +93,21 @@ export default function ThreePanelLayout() {
|
||||
// Keyboard shortcut handler
|
||||
useEffect(() => {
|
||||
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') {
|
||||
e.preventDefault();
|
||||
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);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [setNodesCollapsed]);
|
||||
|
||||
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);
|
||||
}, []);
|
||||
}, [setNodesCollapsed, setChatCollapsed]);
|
||||
|
||||
|
||||
// SSE connection for real-time updates
|
||||
@@ -373,9 +372,16 @@ export default function ThreePanelLayout() {
|
||||
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 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;
|
||||
|
||||
@@ -476,11 +482,15 @@ export default function ThreePanelLayout() {
|
||||
)}
|
||||
|
||||
{/* Middle Panel - Focus */}
|
||||
<div
|
||||
style={{
|
||||
width: `${effectiveMiddleWidth}%`,
|
||||
<div
|
||||
style={{
|
||||
width: chatCollapsed
|
||||
? (nodesCollapsed
|
||||
? `calc(100% - 48px - 40px)`
|
||||
: `calc(${effectiveMiddleWidth}% - 48px)`)
|
||||
: `${effectiveMiddleWidth}%`,
|
||||
flexShrink: 0,
|
||||
borderRight: '1px solid #1a1a1a',
|
||||
borderRight: chatCollapsed ? 'none' : '1px solid #1a1a1a',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -489,8 +499,8 @@ export default function ThreePanelLayout() {
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
<div style={{
|
||||
height: '100%',
|
||||
visibility: folderViewOpen ? 'hidden' : 'visible',
|
||||
pointerEvents: folderViewOpen ? 'none' : 'auto'
|
||||
}}>
|
||||
@@ -516,7 +526,7 @@ export default function ThreePanelLayout() {
|
||||
</div>
|
||||
|
||||
{/* Right Resize Handle */}
|
||||
{!nodesCollapsed && (
|
||||
{!nodesCollapsed && !chatCollapsed && (
|
||||
<div
|
||||
onMouseDown={() => setIsDraggingRight(true)}
|
||||
style={{
|
||||
@@ -530,25 +540,68 @@ export default function ThreePanelLayout() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Right Panel - Agents */}
|
||||
<div
|
||||
style={{
|
||||
width: `${rightWidth}%`,
|
||||
{/* Right Panel - Agents (collapsible) */}
|
||||
{chatCollapsed ? (
|
||||
// Collapsed rail
|
||||
<div style={{
|
||||
width: '48px',
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
borderLeft: '1px solid #2a2a2a',
|
||||
background: '#0f0f0f',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#0a0a0a',
|
||||
position: 'relative',
|
||||
padding: '4px'
|
||||
}}
|
||||
>
|
||||
<AgentsPanel
|
||||
openTabsData={openTabsData}
|
||||
activeTabId={activeNodeId}
|
||||
onNodeClick={(nodeId) => handleNodeSelect(nodeId, false)}
|
||||
/>
|
||||
</div>
|
||||
alignItems: 'center',
|
||||
paddingTop: '12px'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setChatCollapsed(false)}
|
||||
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'
|
||||
}}
|
||||
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 */}
|
||||
<SettingsModal
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
"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 { Node } from '@/types/database';
|
||||
import AddNodeButton from './AddNodeButton';
|
||||
@@ -45,6 +45,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
const [showSearchModal, setShowSearchModal] = useState(false);
|
||||
const [dropFeedback, setDropFeedback] = useState<string | null>(null);
|
||||
const [dragHoverDimension, setDragHoverDimension] = useState<string | null>(null);
|
||||
const draggedNodeRef = useRef<{ id: number; dimensions?: string[] } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchNodes();
|
||||
@@ -153,14 +154,14 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
};
|
||||
|
||||
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.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
@@ -172,17 +173,33 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
event.preventDefault();
|
||||
const raw = event.dataTransfer.getData('application/node-info');
|
||||
if (!raw) return;
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(raw) as { id: number; dimensions?: string[] };
|
||||
if (!payload?.id) return;
|
||||
|
||||
const currentDimensions = payload.dimensions || [];
|
||||
if (currentDimensions.some((dim) => dim.toLowerCase() === dimension.toLowerCase())) {
|
||||
setDropFeedback(`Node already in ${dimension}`);
|
||||
@@ -368,10 +385,15 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
|
||||
const handleNodeDragStart = (event: DragEvent<HTMLElement>, node: Node) => {
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('application/node-info', JSON.stringify({
|
||||
const nodeData = {
|
||||
id: node.id,
|
||||
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');
|
||||
preview.textContent = node.title || `Node #${node.id}`;
|
||||
@@ -394,11 +416,17 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const handleNodeDragEnd = () => {
|
||||
// Clear ref if drag ends without a drop
|
||||
draggedNodeRef.current = null;
|
||||
};
|
||||
|
||||
const renderNodeItem = (node: Node) => (
|
||||
<button
|
||||
key={node.id}
|
||||
draggable
|
||||
onDragStart={(event) => handleNodeDragStart(event, node)}
|
||||
onDragEnd={handleNodeDragEnd}
|
||||
onClick={(e) => {
|
||||
const multiSelect = e.metaKey || e.ctrlKey;
|
||||
onNodeSelect(node.id, multiSelect);
|
||||
@@ -580,27 +608,29 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
<button
|
||||
onClick={() => onToggleFolderView?.()}
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '10px',
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f',
|
||||
background: folderViewOpen ? '#16301f' : '#080808',
|
||||
color: folderViewOpen ? '#22c55e' : '#cbd5f5',
|
||||
background: 'transparent',
|
||||
color: '#666',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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'}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = folderViewOpen ? '#1c3b27' : '#101010';
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
e.currentTarget.style.color = '#999';
|
||||
}}
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user