sync: multiple features from private repo
- quick-add-loading: fire-and-forget with loading placeholders, auto-open feed, SSE events, QuickAddInput redesign - database-table-pane: TablePane + DatabaseTableView, countNodes(), event_date sort - ui-polish-fixes: focus tab order, dim name editing, tab title sync, cost chip removal, custom sort drag-reorder, refresh button, dimension filter removal (~2,200 lines) - node-creation-quality: description service prompt rewrite, title sanitization, extraction tool AI prompt rewrites - feed-pane-ux: stripped kanban/grid/saved views, sort dropdown, AND dimension filtering, description preview Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9954792b1d
commit
2f6518207d
@@ -21,12 +21,21 @@ type AgentDelegation = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export interface PendingNode {
|
||||
id: string;
|
||||
input: string;
|
||||
inputType: string;
|
||||
submittedAt: number;
|
||||
status: 'processing' | 'error';
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// Layout components
|
||||
import LeftToolbar from './LeftToolbar';
|
||||
import SplitHandle from './SplitHandle';
|
||||
|
||||
// Pane components (ChatPane removed in rah-light, GuidesPane moved to settings)
|
||||
import { NodePane, DimensionsPane, MapPane, ViewsPane } from '../panes';
|
||||
import { NodePane, DimensionsPane, MapPane, ViewsPane, TablePane } from '../panes';
|
||||
import QuickAddInput from '../agents/QuickAddInput';
|
||||
import type { PaneType, SlotState, PaneAction } from '../panes/types';
|
||||
|
||||
@@ -99,6 +108,9 @@ export default function ThreePanelLayout() {
|
||||
selectedText: string;
|
||||
} | null>(null);
|
||||
|
||||
// Pending quick-add nodes (loading placeholders)
|
||||
const [pendingNodes, setPendingNodes] = useState<PendingNode[]>([]);
|
||||
|
||||
// Ref to get current openTabs value in SSE handler
|
||||
const openTabsRef = useRef<number[]>([]);
|
||||
|
||||
@@ -164,16 +176,23 @@ export default function ThreePanelLayout() {
|
||||
}
|
||||
};
|
||||
|
||||
// Update tab data whenever openTabs changes (use string key to prevent infinite loops)
|
||||
// Update tab data whenever openTabs changes or focus panel refreshes (use string key to prevent infinite loops)
|
||||
const openTabsKey = openTabs.join(',');
|
||||
useEffect(() => {
|
||||
openTabsRef.current = openTabs;
|
||||
fetchOpenTabsData(openTabs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [openTabsKey]);
|
||||
}, [openTabsKey, focusPanelRefresh]);
|
||||
|
||||
// Delegations loading removed (delegation system removed in rah-light)
|
||||
|
||||
// Refresh all panes
|
||||
const handleRefreshAll = useCallback(() => {
|
||||
setNodesPanelRefresh(prev => prev + 1);
|
||||
setFolderViewRefresh(prev => prev + 1);
|
||||
setFocusPanelRefresh(prev => prev + 1);
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcut handler
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
@@ -192,6 +211,11 @@ export default function ThreePanelLayout() {
|
||||
setSlotB({ type: 'node', nodeTabs: [], activeNodeTab: null });
|
||||
}
|
||||
}
|
||||
// Cmd+Shift+R - refresh all panes
|
||||
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'r') {
|
||||
e.preventDefault();
|
||||
handleRefreshAll();
|
||||
}
|
||||
// Cmd+N - open Add Stuff modal
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
|
||||
// Don't prevent default - browser may want this for new window
|
||||
@@ -205,7 +229,7 @@ export default function ThreePanelLayout() {
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [slotB, setSlotB]);
|
||||
}, [slotB, setSlotB, handleRefreshAll]);
|
||||
|
||||
// SSE connection for real-time updates
|
||||
useEffect(() => {
|
||||
@@ -274,6 +298,22 @@ export default function ThreePanelLayout() {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'QUICK_ADD_COMPLETED':
|
||||
if (data.data?.quickAddId) {
|
||||
setPendingNodes(prev => prev.filter(p => p.id !== data.data.quickAddId));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'QUICK_ADD_FAILED':
|
||||
if (data.data?.quickAddId) {
|
||||
setPendingNodes(prev => prev.map(p =>
|
||||
p.id === data.data.quickAddId
|
||||
? { ...p, status: 'error' as const, error: data.data.error || 'Unknown error' }
|
||||
: p
|
||||
));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'CONNECTION_ESTABLISHED':
|
||||
console.log('✅ SSE connection established');
|
||||
break;
|
||||
@@ -301,6 +341,21 @@ export default function ThreePanelLayout() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Auto-dismiss pending nodes after timeout
|
||||
useEffect(() => {
|
||||
if (pendingNodes.length === 0) return;
|
||||
const interval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
setPendingNodes(prev => prev.filter(p => {
|
||||
const age = now - p.submittedAt;
|
||||
if (p.status === 'processing' && age > 90_000) return false; // 90s timeout
|
||||
if (p.status === 'error' && age > 120_000) return false; // 120s for errors
|
||||
return true;
|
||||
}));
|
||||
}, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [pendingNodes.length]);
|
||||
|
||||
// Node tab management
|
||||
const handleNodeSelect = useCallback((nodeId: number, multiSelect: boolean) => {
|
||||
// If slotA is not a node pane (or doesn't exist), switch it to node
|
||||
@@ -405,27 +460,6 @@ export default function ThreePanelLayout() {
|
||||
setActivePane('A');
|
||||
}, [slotA, setSlotA]);
|
||||
|
||||
// Handle Quick Add submit (used by global Add Stuff modal)
|
||||
const handleQuickAddSubmit = useCallback(async ({ input, mode, description }: { input: string; mode: 'link' | 'note' | 'chat'; description?: string }) => {
|
||||
try {
|
||||
const response = await fetch('/api/quick-add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ input, mode, description })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Failed to submit Quick Add');
|
||||
}
|
||||
|
||||
// Close the modal on success
|
||||
setShowAddStuff(false);
|
||||
} catch (error) {
|
||||
console.error('[ThreePanelLayout] Quick Add error:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleNodeDeleted = useCallback((nodeId: number) => {
|
||||
handleCloseTab(nodeId);
|
||||
}, [handleCloseTab]);
|
||||
@@ -494,6 +528,51 @@ export default function ThreePanelLayout() {
|
||||
}
|
||||
}, [activePane, slotA, slotB, setSlotA, setSlotB]);
|
||||
|
||||
// Ensure the Feed pane is visible (for quick-add loading placeholders)
|
||||
const ensureFeedOpen = useCallback(() => {
|
||||
if (slotA?.type === 'views') return;
|
||||
if (slotB?.type === 'views') return;
|
||||
handlePaneTypeClick('views');
|
||||
}, [slotA, slotB, handlePaneTypeClick]);
|
||||
|
||||
// Handle Quick Add submit (used by global Add Stuff modal)
|
||||
const handleQuickAddSubmit = useCallback(async ({ input, mode, description }: { input: string; mode: 'link' | 'note' | 'chat'; description?: string }) => {
|
||||
try {
|
||||
const response = await fetch('/api/quick-add', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ input, mode, description })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || 'Failed to submit Quick Add');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
const delegation = result.delegation;
|
||||
|
||||
// Add pending placeholder
|
||||
if (delegation?.id && delegation?.inputType) {
|
||||
setPendingNodes(prev => [{
|
||||
id: delegation.id,
|
||||
input: input.slice(0, 120),
|
||||
inputType: delegation.inputType,
|
||||
submittedAt: Date.now(),
|
||||
status: 'processing' as const,
|
||||
}, ...prev]);
|
||||
}
|
||||
|
||||
// Ensure feed pane is visible
|
||||
ensureFeedOpen();
|
||||
|
||||
// Close the modal on success
|
||||
setShowAddStuff(false);
|
||||
} catch (error) {
|
||||
console.error('[ThreePanelLayout] Quick Add error:', error);
|
||||
}
|
||||
}, [ensureFeedOpen]);
|
||||
|
||||
// Handle closing a pane
|
||||
const handleCloseSlotA = useCallback(() => {
|
||||
if (slotB) {
|
||||
@@ -860,6 +939,24 @@ export default function ThreePanelLayout() {
|
||||
}}
|
||||
onNodeOpenInOtherPane={slot === 'A' ? handleNodeOpenInSlotB : handleNodeOpenInSlotA}
|
||||
refreshToken={nodesPanelRefresh}
|
||||
pendingNodes={pendingNodes}
|
||||
onDismissPending={(id) => setPendingNodes(prev => prev.filter(p => p.id !== id))}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'table':
|
||||
return (
|
||||
<TablePane
|
||||
slot={slot}
|
||||
isActive={isActive}
|
||||
onPaneAction={slot === 'A' ? handleSlotAAction : handleSlotBAction}
|
||||
onCollapse={onCollapse}
|
||||
onSwapPanes={slotB ? handleSwapPanes : undefined}
|
||||
onNodeClick={(nodeId) => {
|
||||
handleNodeSelect(nodeId, false);
|
||||
setActivePane(slot);
|
||||
}}
|
||||
refreshToken={nodesPanelRefresh}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -892,6 +989,7 @@ export default function ThreePanelLayout() {
|
||||
activePane={activePane}
|
||||
slotAType={slotA?.type ?? null}
|
||||
slotBType={slotB?.type ?? null}
|
||||
onRefreshClick={handleRefreshAll}
|
||||
/>
|
||||
|
||||
{/* Main content area */}
|
||||
@@ -931,7 +1029,7 @@ export default function ThreePanelLayout() {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// Center single pane (except map)
|
||||
...((!slotB && slotA.type !== 'map') ? {
|
||||
...((!slotB && slotA.type !== 'map' && slotA.type !== 'table') ? {
|
||||
maxWidth: '900px',
|
||||
margin: '0 auto',
|
||||
width: '100%',
|
||||
|
||||
Reference in New Issue
Block a user