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,
onCloseTabsRight,
// Unified tab system props (Phase 3)
unifiedTabs: _unifiedTabs,
activeFileTabId: _activeFileTabId,
onFileTabSelect: _onFileTabSelect,
onFileTabClose: _onFileTabClose,
unifiedTabs,
activeFileTabId,
onFileTabSelect,
onFileTabClose,
onUnifiedTabReorder,
}: TabBarProps) {
const [draggingTabId, setDraggingTabId] = 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;
// 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) => {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', tabId);
@@ -1250,7 +1266,17 @@ function TabBarInner({
e.preventDefault();
const sourceTabId = e.dataTransfer.getData('text/plain');
if (sourceTabId && sourceTabId !== targetTabId && onTabReorder) {
if (sourceTabId && sourceTabId !== targetTabId) {
// When unified tabs are used, prefer onUnifiedTabReorder
if (unifiedTabs && onUnifiedTabReorder) {
const sourceIndex = unifiedTabs.findIndex((ut) => ut.id === sourceTabId);
const targetIndex = unifiedTabs.findIndex((ut) => ut.id === targetTabId);
if (sourceIndex !== -1 && targetIndex !== -1) {
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);
@@ -1258,11 +1284,12 @@ function TabBarInner({
onTabReorder(sourceIndex, targetIndex);
}
}
}
setDraggingTabId(null);
setDragOverTabId(null);
},
[tabs, onTabReorder]
[tabs, onTabReorder, unifiedTabs, onUnifiedTabReorder]
);
const handleRenameRequest = useCallback(
@@ -1293,28 +1320,44 @@ function TabBarInner({
clearTimeout(timeoutId);
window.removeEventListener('resize', checkOverflow);
};
}, [tabs.length, displayedTabs.length]);
}, [tabs.length, displayedTabs.length, unifiedTabs?.length, displayedUnifiedTabs?.length]);
const handleMoveToFirst = useCallback(
(tabId: string) => {
// Find the current index in the FULL tabs array (not filtered)
// When unified tabs are used, prefer onUnifiedTabReorder
if (unifiedTabs && onUnifiedTabReorder) {
const currentIndex = unifiedTabs.findIndex((ut) => ut.id === tabId);
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) {
if (currentIndex > 0) {
onTabReorder(currentIndex, 0);
}
}
},
[tabs, onTabReorder]
[tabs, onTabReorder, unifiedTabs, onUnifiedTabReorder]
);
const handleMoveToLast = useCallback(
(tabId: string) => {
// Find the current index in the FULL tabs array (not filtered)
// When unified tabs are used, prefer onUnifiedTabReorder
if (unifiedTabs && onUnifiedTabReorder) {
const currentIndex = unifiedTabs.findIndex((ut) => ut.id === tabId);
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) {
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
@@ -1453,7 +1496,8 @@ function TabBarInner({
</div>
{/* Empty state when filter is on but no unread tabs */}
{showUnreadOnly && displayedTabs.length === 0 && (
{showUnreadOnly &&
(displayedUnifiedTabs ? displayedUnifiedTabs.length === 0 : displayedTabs.length === 0) && (
<div
className="flex items-center px-3 py-1.5 text-xs italic shrink-0 self-center mb-1"
style={{ color: theme.colors.textDim }}
@@ -1463,7 +1507,127 @@ function TabBarInner({
)}
{/* Tabs with separators between inactive tabs */}
{displayedTabs.map((tab, index) => {
{/* When unifiedTabs is provided, render both AI and file tabs from unified list */}
{displayedUnifiedTabs
? displayedUnifiedTabs.map((unifiedTab, index) => {
// Determine if this tab is active (based on type)
const isActive =
unifiedTab.type === 'ai'
? unifiedTab.id === activeTabId
: unifiedTab.id === activeFileTabId;
// Check previous tab's active state for separator logic
const prevUnifiedTab = index > 0 ? displayedUnifiedTabs[index - 1] : null;
const isPrevActive = prevUnifiedTab
? prevUnifiedTab.type === 'ai'
? prevUnifiedTab.id === activeTabId
: prevUnifiedTab.id === activeFileTabId
: false;
// Get original index in the FULL unified list (not filtered)
const originalIndex = unifiedTabs!.findIndex((ut) => ut.id === unifiedTab.id);
// Show separator between inactive tabs
const showSeparator = index > 0 && !isActive && !isPrevActive;
// Position info for move actions
const isFirstTab = originalIndex === 0;
const isLastTab = originalIndex === unifiedTabs!.length - 1;
if (unifiedTab.type === 'ai') {
const tab = unifiedTab.data;
return (
<React.Fragment key={unifiedTab.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 && onUnifiedTabReorder ? handleMoveToFirst : undefined}
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;
@@ -1503,14 +1667,18 @@ function TabBarInner({
onStar={onTabStar && tab.agentSessionId ? handleTabStar : undefined}
onMarkUnread={onTabMarkUnread ? handleTabMarkUnread : undefined}
onMergeWith={onMergeWith && tab.agentSessionId ? handleTabMergeWith : undefined}
onSendToAgent={onSendToAgent && tab.agentSessionId ? handleTabSendToAgent : 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
onCopyContext && (tab.logs?.length ?? 0) >= 1
? handleTabCopyContext
: undefined
}
onExportHtml={onExportHtml ? handleTabExportHtml : undefined}
onPublishGist={