MAESTRO: Update TabBar render loop to iterate over unified tabs

- Add displayedUnifiedTabs computed value with unread filter support
- Update render loop to conditionally render from unified tabs when provided
- Check unified tab type to render either Tab (AI) or FileTab (file)
- Update handleDrop, handleMoveToFirst, handleMoveToLast for unified tabs
- Update overflow check and empty state to consider unified tabs
- Maintain backwards compatibility with legacy AI-only tab rendering
This commit is contained in:
Pedram Amini
2026-02-02 03:49:59 -06:00
parent d7004d6101
commit 495ed0f60f
2 changed files with 312 additions and 646 deletions

594
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1177,10 +1177,11 @@ function TabBarInner({
onCloseTabsLeft, onCloseTabsLeft,
onCloseTabsRight, onCloseTabsRight,
// Unified tab system props (Phase 3) // Unified tab system props (Phase 3)
unifiedTabs: _unifiedTabs, unifiedTabs,
activeFileTabId: _activeFileTabId, activeFileTabId,
onFileTabSelect: _onFileTabSelect, onFileTabSelect,
onFileTabClose: _onFileTabClose, onFileTabClose,
onUnifiedTabReorder,
}: TabBarProps) { }: TabBarProps) {
const [draggingTabId, setDraggingTabId] = useState<string | null>(null); const [draggingTabId, setDraggingTabId] = useState<string | null>(null);
const [dragOverTabId, setDragOverTabId] = useState<string | null>(null); const [dragOverTabId, setDragOverTabId] = useState<string | null>(null);
@@ -1223,6 +1224,21 @@ function TabBarInner({
? tabs.filter((t) => t.hasUnread || t.id === activeTabId || hasDraft(t)) ? tabs.filter((t) => t.hasUnread || t.id === activeTabId || hasDraft(t))
: tabs; : tabs;
// When unifiedTabs is provided, filter it similarly for display
// File tabs don't have "unread" state, so they only show in filtered mode if active
const displayedUnifiedTabs = useMemo(() => {
if (!unifiedTabs) return null;
if (!showUnreadOnly) return unifiedTabs;
// In filter mode: show AI tabs that are unread/active/have drafts, plus file tabs that are active
return unifiedTabs.filter((ut) => {
if (ut.type === 'ai') {
return ut.data.hasUnread || ut.id === activeTabId || hasDraft(ut.data);
}
// File tabs: only show if active
return ut.id === activeFileTabId;
});
}, [unifiedTabs, showUnreadOnly, activeTabId, activeFileTabId]);
const handleDragStart = useCallback((tabId: string, e: React.DragEvent) => { const handleDragStart = useCallback((tabId: string, e: React.DragEvent) => {
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', tabId); e.dataTransfer.setData('text/plain', tabId);
@@ -1250,19 +1266,30 @@ function TabBarInner({
e.preventDefault(); e.preventDefault();
const sourceTabId = e.dataTransfer.getData('text/plain'); const sourceTabId = e.dataTransfer.getData('text/plain');
if (sourceTabId && sourceTabId !== targetTabId && onTabReorder) { if (sourceTabId && sourceTabId !== targetTabId) {
const sourceIndex = tabs.findIndex((t) => t.id === sourceTabId); // When unified tabs are used, prefer onUnifiedTabReorder
const targetIndex = tabs.findIndex((t) => t.id === targetTabId); if (unifiedTabs && onUnifiedTabReorder) {
const sourceIndex = unifiedTabs.findIndex((ut) => ut.id === sourceTabId);
const targetIndex = unifiedTabs.findIndex((ut) => ut.id === targetTabId);
if (sourceIndex !== -1 && targetIndex !== -1) { if (sourceIndex !== -1 && targetIndex !== -1) {
onTabReorder(sourceIndex, targetIndex); onUnifiedTabReorder(sourceIndex, targetIndex);
}
} else if (onTabReorder) {
// Fallback to legacy AI-tab-only reorder
const sourceIndex = tabs.findIndex((t) => t.id === sourceTabId);
const targetIndex = tabs.findIndex((t) => t.id === targetTabId);
if (sourceIndex !== -1 && targetIndex !== -1) {
onTabReorder(sourceIndex, targetIndex);
}
} }
} }
setDraggingTabId(null); setDraggingTabId(null);
setDragOverTabId(null); setDragOverTabId(null);
}, },
[tabs, onTabReorder] [tabs, onTabReorder, unifiedTabs, onUnifiedTabReorder]
); );
const handleRenameRequest = useCallback( const handleRenameRequest = useCallback(
@@ -1293,28 +1320,44 @@ function TabBarInner({
clearTimeout(timeoutId); clearTimeout(timeoutId);
window.removeEventListener('resize', checkOverflow); window.removeEventListener('resize', checkOverflow);
}; };
}, [tabs.length, displayedTabs.length]); }, [tabs.length, displayedTabs.length, unifiedTabs?.length, displayedUnifiedTabs?.length]);
const handleMoveToFirst = useCallback( const handleMoveToFirst = useCallback(
(tabId: string) => { (tabId: string) => {
// Find the current index in the FULL tabs array (not filtered) // When unified tabs are used, prefer onUnifiedTabReorder
const currentIndex = tabs.findIndex((t) => t.id === tabId); if (unifiedTabs && onUnifiedTabReorder) {
if (currentIndex > 0 && onTabReorder) { const currentIndex = unifiedTabs.findIndex((ut) => ut.id === tabId);
onTabReorder(currentIndex, 0); if (currentIndex > 0) {
onUnifiedTabReorder(currentIndex, 0);
}
} else if (onTabReorder) {
// Fallback to legacy AI-tab-only reorder
const currentIndex = tabs.findIndex((t) => t.id === tabId);
if (currentIndex > 0) {
onTabReorder(currentIndex, 0);
}
} }
}, },
[tabs, onTabReorder] [tabs, onTabReorder, unifiedTabs, onUnifiedTabReorder]
); );
const handleMoveToLast = useCallback( const handleMoveToLast = useCallback(
(tabId: string) => { (tabId: string) => {
// Find the current index in the FULL tabs array (not filtered) // When unified tabs are used, prefer onUnifiedTabReorder
const currentIndex = tabs.findIndex((t) => t.id === tabId); if (unifiedTabs && onUnifiedTabReorder) {
if (currentIndex < tabs.length - 1 && onTabReorder) { const currentIndex = unifiedTabs.findIndex((ut) => ut.id === tabId);
onTabReorder(currentIndex, tabs.length - 1); if (currentIndex < unifiedTabs.length - 1) {
onUnifiedTabReorder(currentIndex, unifiedTabs.length - 1);
}
} else if (onTabReorder) {
// Fallback to legacy AI-tab-only reorder
const currentIndex = tabs.findIndex((t) => t.id === tabId);
if (currentIndex < tabs.length - 1) {
onTabReorder(currentIndex, tabs.length - 1);
}
} }
}, },
[tabs, onTabReorder] [tabs, onTabReorder, unifiedTabs, onUnifiedTabReorder]
); );
// Stable callback wrappers that receive tabId from the Tab component // Stable callback wrappers that receive tabId from the Tab component
@@ -1453,88 +1496,213 @@ function TabBarInner({
</div> </div>
{/* Empty state when filter is on but no unread tabs */} {/* Empty state when filter is on but no unread tabs */}
{showUnreadOnly && displayedTabs.length === 0 && ( {showUnreadOnly &&
<div (displayedUnifiedTabs ? displayedUnifiedTabs.length === 0 : displayedTabs.length === 0) && (
className="flex items-center px-3 py-1.5 text-xs italic shrink-0 self-center mb-1" <div
style={{ color: theme.colors.textDim }} className="flex items-center px-3 py-1.5 text-xs italic shrink-0 self-center mb-1"
> style={{ color: theme.colors.textDim }}
No unread tabs >
</div> No unread tabs
)} </div>
)}
{/* Tabs with separators between inactive tabs */} {/* Tabs with separators between inactive tabs */}
{displayedTabs.map((tab, index) => { {/* When unifiedTabs is provided, render both AI and file tabs from unified list */}
const isActive = tab.id === activeTabId; {displayedUnifiedTabs
const prevTab = index > 0 ? displayedTabs[index - 1] : null; ? displayedUnifiedTabs.map((unifiedTab, index) => {
const isPrevActive = prevTab?.id === activeTabId; // Determine if this tab is active (based on type)
// Get original index for shortcut hints (Cmd+1-9) const isActive =
const originalIndex = tabs.findIndex((t) => t.id === tab.id); unifiedTab.type === 'ai'
? unifiedTab.id === activeTabId
: unifiedTab.id === activeFileTabId;
// Show separator between inactive tabs (not adjacent to active tab) // Check previous tab's active state for separator logic
const showSeparator = index > 0 && !isActive && !isPrevActive; const prevUnifiedTab = index > 0 ? displayedUnifiedTabs[index - 1] : null;
const isPrevActive = prevUnifiedTab
? prevUnifiedTab.type === 'ai'
? prevUnifiedTab.id === activeTabId
: prevUnifiedTab.id === activeFileTabId
: false;
// Calculate position info for move actions (within FULL tabs array, not filtered) // Get original index in the FULL unified list (not filtered)
const isFirstTab = originalIndex === 0; const originalIndex = unifiedTabs!.findIndex((ut) => ut.id === unifiedTab.id);
const isLastTab = originalIndex === tabs.length - 1;
return ( // Show separator between inactive tabs
<React.Fragment key={tab.id}> const showSeparator = index > 0 && !isActive && !isPrevActive;
{showSeparator && (
<div // Position info for move actions
className="w-px h-4 self-center shrink-0" const isFirstTab = originalIndex === 0;
style={{ backgroundColor: theme.colors.border }} const isLastTab = originalIndex === unifiedTabs!.length - 1;
/>
)} if (unifiedTab.type === 'ai') {
<Tab const tab = unifiedTab.data;
tab={tab} return (
tabId={tab.id} <React.Fragment key={unifiedTab.id}>
isActive={isActive} {showSeparator && (
theme={theme} <div
canClose={canClose} className="w-px h-4 self-center shrink-0"
onSelect={onTabSelect} style={{ backgroundColor: theme.colors.border }}
onClose={onTabClose} />
onDragStart={handleDragStart} )}
onDragOver={handleDragOver} <Tab
onDragEnd={handleDragEnd} tab={tab}
onDrop={handleDrop} tabId={tab.id}
isDragging={draggingTabId === tab.id} isActive={isActive}
isDragOver={dragOverTabId === tab.id} theme={theme}
onRename={handleRenameRequest} canClose={canClose}
onStar={onTabStar && tab.agentSessionId ? handleTabStar : undefined} onSelect={onTabSelect}
onMarkUnread={onTabMarkUnread ? handleTabMarkUnread : undefined} onClose={onTabClose}
onMergeWith={onMergeWith && tab.agentSessionId ? handleTabMergeWith : undefined} onDragStart={handleDragStart}
onSendToAgent={onSendToAgent && tab.agentSessionId ? handleTabSendToAgent : undefined} onDragOver={handleDragOver}
onSummarizeAndContinue={ onDragEnd={handleDragEnd}
onSummarizeAndContinue && (tab.logs?.length ?? 0) >= 5 onDrop={handleDrop}
? handleTabSummarizeAndContinue isDragging={draggingTabId === tab.id}
: undefined isDragOver={dragOverTabId === tab.id}
} onRename={handleRenameRequest}
onCopyContext={ onStar={onTabStar && tab.agentSessionId ? handleTabStar : undefined}
onCopyContext && (tab.logs?.length ?? 0) >= 1 ? handleTabCopyContext : undefined onMarkUnread={onTabMarkUnread ? handleTabMarkUnread : undefined}
} onMergeWith={onMergeWith && tab.agentSessionId ? handleTabMergeWith : undefined}
onExportHtml={onExportHtml ? handleTabExportHtml : undefined} onSendToAgent={
onPublishGist={ onSendToAgent && tab.agentSessionId ? handleTabSendToAgent : undefined
onPublishGist && ghCliAvailable && (tab.logs?.length ?? 0) >= 1 }
? handleTabPublishGist onSummarizeAndContinue={
: undefined onSummarizeAndContinue && (tab.logs?.length ?? 0) >= 5
} ? handleTabSummarizeAndContinue
onMoveToFirst={!isFirstTab && onTabReorder ? handleMoveToFirst : undefined} : undefined
onMoveToLast={!isLastTab && onTabReorder ? handleMoveToLast : undefined} }
isFirstTab={isFirstTab} onCopyContext={
isLastTab={isLastTab} onCopyContext && (tab.logs?.length ?? 0) >= 1
shortcutHint={!showUnreadOnly && originalIndex < 9 ? originalIndex + 1 : null} ? handleTabCopyContext
hasDraft={hasDraft(tab)} : undefined
registerRef={(el) => registerTabRef(tab.id, el)} }
onCloseAllTabs={onCloseAllTabs} onExportHtml={onExportHtml ? handleTabExportHtml : undefined}
onCloseOtherTabs={onCloseOtherTabs ? handleTabCloseOther : undefined} onPublishGist={
onCloseTabsLeft={onCloseTabsLeft ? handleTabCloseLeft : undefined} onPublishGist && ghCliAvailable && (tab.logs?.length ?? 0) >= 1
onCloseTabsRight={onCloseTabsRight ? handleTabCloseRight : undefined} ? handleTabPublishGist
totalTabs={tabs.length} : undefined
tabIndex={originalIndex} }
/> onMoveToFirst={!isFirstTab && onUnifiedTabReorder ? handleMoveToFirst : undefined}
</React.Fragment> onMoveToLast={!isLastTab && onUnifiedTabReorder ? handleMoveToLast : undefined}
); isFirstTab={isFirstTab}
})} isLastTab={isLastTab}
shortcutHint={!showUnreadOnly && originalIndex < 9 ? originalIndex + 1 : null}
hasDraft={hasDraft(tab)}
registerRef={(el) => registerTabRef(tab.id, el)}
onCloseAllTabs={onCloseAllTabs}
onCloseOtherTabs={onCloseOtherTabs ? handleTabCloseOther : undefined}
onCloseTabsLeft={onCloseTabsLeft ? handleTabCloseLeft : undefined}
onCloseTabsRight={onCloseTabsRight ? handleTabCloseRight : undefined}
totalTabs={unifiedTabs!.length}
tabIndex={originalIndex}
/>
</React.Fragment>
);
} else {
// File tab
const fileTab = unifiedTab.data;
return (
<React.Fragment key={unifiedTab.id}>
{showSeparator && (
<div
className="w-px h-4 self-center shrink-0"
style={{ backgroundColor: theme.colors.border }}
/>
)}
<FileTab
tab={fileTab}
isActive={isActive}
theme={theme}
onSelect={onFileTabSelect || (() => {})}
onClose={onFileTabClose || (() => {})}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDrop={handleDrop}
isDragging={draggingTabId === fileTab.id}
isDragOver={dragOverTabId === fileTab.id}
registerRef={(el) => registerTabRef(fileTab.id, el)}
/>
</React.Fragment>
);
}
})
: // Fallback: render AI tabs only (legacy mode when unifiedTabs not provided)
displayedTabs.map((tab, index) => {
const isActive = tab.id === activeTabId;
const prevTab = index > 0 ? displayedTabs[index - 1] : null;
const isPrevActive = prevTab?.id === activeTabId;
// Get original index for shortcut hints (Cmd+1-9)
const originalIndex = tabs.findIndex((t) => t.id === tab.id);
// Show separator between inactive tabs (not adjacent to active tab)
const showSeparator = index > 0 && !isActive && !isPrevActive;
// Calculate position info for move actions (within FULL tabs array, not filtered)
const isFirstTab = originalIndex === 0;
const isLastTab = originalIndex === tabs.length - 1;
return (
<React.Fragment key={tab.id}>
{showSeparator && (
<div
className="w-px h-4 self-center shrink-0"
style={{ backgroundColor: theme.colors.border }}
/>
)}
<Tab
tab={tab}
tabId={tab.id}
isActive={isActive}
theme={theme}
canClose={canClose}
onSelect={onTabSelect}
onClose={onTabClose}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDrop={handleDrop}
isDragging={draggingTabId === tab.id}
isDragOver={dragOverTabId === tab.id}
onRename={handleRenameRequest}
onStar={onTabStar && tab.agentSessionId ? handleTabStar : undefined}
onMarkUnread={onTabMarkUnread ? handleTabMarkUnread : undefined}
onMergeWith={onMergeWith && tab.agentSessionId ? handleTabMergeWith : undefined}
onSendToAgent={
onSendToAgent && tab.agentSessionId ? handleTabSendToAgent : undefined
}
onSummarizeAndContinue={
onSummarizeAndContinue && (tab.logs?.length ?? 0) >= 5
? handleTabSummarizeAndContinue
: undefined
}
onCopyContext={
onCopyContext && (tab.logs?.length ?? 0) >= 1
? handleTabCopyContext
: undefined
}
onExportHtml={onExportHtml ? handleTabExportHtml : undefined}
onPublishGist={
onPublishGist && ghCliAvailable && (tab.logs?.length ?? 0) >= 1
? handleTabPublishGist
: undefined
}
onMoveToFirst={!isFirstTab && onTabReorder ? handleMoveToFirst : undefined}
onMoveToLast={!isLastTab && onTabReorder ? handleMoveToLast : undefined}
isFirstTab={isFirstTab}
isLastTab={isLastTab}
shortcutHint={!showUnreadOnly && originalIndex < 9 ? originalIndex + 1 : null}
hasDraft={hasDraft(tab)}
registerRef={(el) => registerTabRef(tab.id, el)}
onCloseAllTabs={onCloseAllTabs}
onCloseOtherTabs={onCloseOtherTabs ? handleTabCloseOther : undefined}
onCloseTabsLeft={onCloseTabsLeft ? handleTabCloseLeft : undefined}
onCloseTabsRight={onCloseTabsRight ? handleTabCloseRight : undefined}
totalTabs={tabs.length}
tabIndex={originalIndex}
/>
</React.Fragment>
);
})}
{/* New Tab Button - sticky on right when tabs overflow, with full-height opaque background */} {/* New Tab Button - sticky on right when tabs overflow, with full-height opaque background */}
<div <div