diff --git a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
index 2e67c94b2f..4300aef499 100644
--- a/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
+++ b/invokeai/frontend/web/src/features/ui/components/AppContent.tsx
@@ -16,14 +16,13 @@ import {
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
import { CanvasTabAutoLayout } from 'features/ui/layouts/canvas-tab-auto-layout';
import { GenerateTabAutoLayout } from 'features/ui/layouts/generate-tab-auto-layout';
+import { ModelsTabAutoLayout } from 'features/ui/layouts/models-tab-auto-layout';
+import { QueueTabAutoLayout } from 'features/ui/layouts/queue-tab-auto-layout';
import { UpscalingTabAutoLayout } from 'features/ui/layouts/upscaling-tab-auto-layout';
import { WorkflowsTabAutoLayout } from 'features/ui/layouts/workflows-tab-auto-layout';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { memo, useState } from 'react';
-import ModelManagerTab from './tabs/ModelManagerTab';
-import QueueTab from './tabs/QueueTab';
-
export const AppContent = memo(() => {
useDndMonitor();
const tab = useAppSelector(selectActiveTab);
@@ -43,8 +42,8 @@ export const AppContent = memo(() => {
{withCanvasTab && tab === 'canvas' && }
{withUpscalingTab && tab === 'upscaling' && }
{withWorkflowsTab && tab === 'workflows' && }
- {withModelsTab && tab === 'models' && }
- {withQueueTab && tab === 'queue' && }
+ {withModelsTab && tab === 'models' && }
+ {withQueueTab && tab === 'queue' && }
{isLoading && }
diff --git a/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx
new file mode 100644
index 0000000000..f45edcd486
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/models-tab-auto-layout.tsx
@@ -0,0 +1,64 @@
+import type { GridviewApi, IGridviewPanel, IGridviewReactProps } from 'dockview';
+import { GridviewReact, LayoutPriority, Orientation } from 'dockview';
+import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
+import type { RootLayoutGridviewComponents } from 'features/ui/layouts/auto-layout-context';
+import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { memo, useCallback, useEffect, useRef, useState } from 'react';
+
+import { navigationApi } from './navigation-api';
+import { MODELS_PANEL_ID } from './shared';
+
+export const rootPanelComponents: RootLayoutGridviewComponents = {
+ [MODELS_PANEL_ID]: ModelManagerTab,
+};
+
+export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
+ const models = layoutApi.addPanel({
+ id: MODELS_PANEL_ID,
+ component: MODELS_PANEL_ID,
+ priority: LayoutPriority.High,
+ });
+
+ navigationApi.registerPanel('models', MODELS_PANEL_ID, models);
+
+ return { models } satisfies Record;
+};
+
+export const ModelsTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => {
+ const rootRef = useRef(null);
+ const [rootApi, setRootApi] = useState(null);
+ const onReady = useCallback(({ api }) => {
+ setRootApi(api);
+ }, []);
+
+ useEffect(() => {
+ setIsLoading(true);
+
+ if (!rootApi) {
+ return;
+ }
+
+ initializeRootPanelLayout(rootApi);
+
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 300);
+
+ return () => {
+ navigationApi.unregisterTab('models');
+ };
+ }, [rootApi, setIsLoading]);
+
+ return (
+
+
+
+ );
+});
+ModelsTabAutoLayout.displayName = 'ModelsTabAutoLayout';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.test.ts b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.test.ts
index dde2d147c3..5ce399a1c8 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.test.ts
+++ b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.test.ts
@@ -2,7 +2,15 @@ import { GridviewPanel, type IDockviewPanel } from 'dockview';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { NavigationApi } from './navigation-api';
-import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from './shared';
+import {
+ LAUNCHPAD_PANEL_ID,
+ LEFT_PANEL_ID,
+ LEFT_PANEL_MIN_SIZE_PX,
+ RIGHT_PANEL_ID,
+ RIGHT_PANEL_MIN_SIZE_PX,
+ SETTINGS_PANEL_ID,
+ WORKSPACE_PANEL_ID,
+} from './shared';
// Mock the logger
vi.mock('app/logging/logger', () => ({
@@ -91,24 +99,24 @@ describe('AppNavigationApi', () => {
describe('Panel Registration', () => {
it('should register and unregister panels', () => {
const mockPanel = createMockPanel();
- const unregister = navigationApi.registerPanel('generate', 'settings', mockPanel);
+ const unregister = navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
expect(typeof unregister).toBe('function');
- expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(true);
+ expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(true);
// Unregister
unregister();
- expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(false);
+ expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(false);
});
it('should resolve waiting promises when panel is registered', async () => {
const mockPanel = createMockPanel();
// Start waiting for registration
- const waitPromise = navigationApi.waitForPanel('generate', 'settings');
+ const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID);
// Register the panel
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
// Wait should resolve
await expect(waitPromise).resolves.toBeUndefined();
@@ -118,30 +126,30 @@ describe('AppNavigationApi', () => {
const mockPanel1 = createMockPanel();
const mockPanel2 = createMockDockPanel();
- navigationApi.registerPanel('generate', 'settings', mockPanel1);
- navigationApi.registerPanel('generate', 'launchpad', mockPanel2);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
+ navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2);
- expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(true);
- expect(navigationApi.isPanelRegistered('generate', 'launchpad')).toBe(true);
+ expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(true);
+ expect(navigationApi.isPanelRegistered('generate', LAUNCHPAD_PANEL_ID)).toBe(true);
const registeredPanels = navigationApi.getRegisteredPanels('generate');
- expect(registeredPanels).toContain('settings');
- expect(registeredPanels).toContain('launchpad');
+ expect(registeredPanels).toContain(SETTINGS_PANEL_ID);
+ expect(registeredPanels).toContain(LAUNCHPAD_PANEL_ID);
});
it('should handle panels across different tabs', () => {
const mockPanel1 = createMockPanel();
const mockPanel2 = createMockPanel();
- navigationApi.registerPanel('generate', 'settings', mockPanel1);
- navigationApi.registerPanel('canvas', 'settings', mockPanel2);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
+ navigationApi.registerPanel('canvas', SETTINGS_PANEL_ID, mockPanel2);
- expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(true);
- expect(navigationApi.isPanelRegistered('canvas', 'settings')).toBe(true);
+ expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(true);
+ expect(navigationApi.isPanelRegistered('canvas', SETTINGS_PANEL_ID)).toBe(true);
// Same panel ID in different tabs should be separate
- expect(navigationApi.getRegisteredPanels('generate')).toEqual(['settings']);
- expect(navigationApi.getRegisteredPanels('canvas')).toEqual(['settings']);
+ expect(navigationApi.getRegisteredPanels('generate')).toEqual([SETTINGS_PANEL_ID]);
+ expect(navigationApi.getRegisteredPanels('canvas')).toEqual([SETTINGS_PANEL_ID]);
});
});
@@ -152,10 +160,10 @@ describe('AppNavigationApi', () => {
it('should focus panel in already registered tab', async () => {
const mockPanel = createMockPanel();
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('generate');
- const result = await navigationApi.focusPanel('generate', 'settings');
+ const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
expect(result).toBe(true);
expect(mockSetAppTab).not.toHaveBeenCalled();
@@ -164,10 +172,10 @@ describe('AppNavigationApi', () => {
it('should switch tab before focusing panel', async () => {
const mockPanel = createMockPanel();
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('canvas'); // Currently on different tab
- const result = await navigationApi.focusPanel('generate', 'settings');
+ const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
expect(result).toBe(true);
expect(mockSetAppTab).toHaveBeenCalledWith('generate');
@@ -179,11 +187,11 @@ describe('AppNavigationApi', () => {
mockGetAppTab.mockReturnValue('generate');
// Start focus operation before panel is registered
- const focusPromise = navigationApi.focusPanel('generate', 'settings');
+ const focusPromise = navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
// Register panel after a short delay
setTimeout(() => {
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
}, 100);
const result = await focusPromise;
@@ -196,17 +204,17 @@ describe('AppNavigationApi', () => {
const mockGridPanel = createMockPanel();
const mockDockPanel = createMockDockPanel();
- navigationApi.registerPanel('generate', 'settings', mockGridPanel);
- navigationApi.registerPanel('generate', 'launchpad', mockDockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockGridPanel);
+ navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockDockPanel);
mockGetAppTab.mockReturnValue('generate');
// Test gridview panel
- const result1 = await navigationApi.focusPanel('generate', 'settings');
+ const result1 = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
expect(result1).toBe(true);
expect(mockGridPanel.api.setActive).toHaveBeenCalledOnce();
// Test dockview panel
- const result2 = await navigationApi.focusPanel('generate', 'launchpad');
+ const result2 = await navigationApi.focusPanel('generate', LAUNCHPAD_PANEL_ID);
expect(result2).toBe(true);
expect(mockDockPanel.api.setActive).toHaveBeenCalledOnce();
});
@@ -215,7 +223,7 @@ describe('AppNavigationApi', () => {
mockGetAppTab.mockReturnValue('generate');
// Set a short timeout for testing
- const result = await navigationApi.focusPanel('generate', 'settings');
+ const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
expect(result).toBe(false);
});
@@ -228,20 +236,20 @@ describe('AppNavigationApi', () => {
throw new Error('Mock error');
});
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('generate');
- const result = await navigationApi.focusPanel('generate', 'settings');
+ const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
expect(result).toBe(false);
});
it('should work without app connection', async () => {
const mockPanel = createMockPanel();
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
// Don't connect to app
- const result = await navigationApi.focusPanel('generate', 'settings');
+ const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
expect(result).toBe(true);
expect(mockPanel.api.setActive).toHaveBeenCalledOnce();
@@ -251,9 +259,9 @@ describe('AppNavigationApi', () => {
describe('Panel Waiting', () => {
it('should resolve immediately for already registered panels', async () => {
const mockPanel = createMockPanel();
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
- const waitPromise = navigationApi.waitForPanel('generate', 'settings');
+ const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID);
await expect(waitPromise).resolves.toBeUndefined();
});
@@ -261,25 +269,25 @@ describe('AppNavigationApi', () => {
it('should handle multiple waiters for same panel', async () => {
const mockPanel = createMockPanel();
- const waitPromise1 = navigationApi.waitForPanel('generate', 'settings');
- const waitPromise2 = navigationApi.waitForPanel('generate', 'settings');
+ const waitPromise1 = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID);
+ const waitPromise2 = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID);
setTimeout(() => {
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
}, 50);
await expect(Promise.all([waitPromise1, waitPromise2])).resolves.toEqual([undefined, undefined]);
});
it('should timeout if panel is not registered', async () => {
- const waitPromise = navigationApi.waitForPanel('generate', 'settings', 100);
+ const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID, 100);
await expect(waitPromise).rejects.toThrow('Panel generate:settings registration timed out after 100ms');
});
it('should handle custom timeout', async () => {
const start = Date.now();
- const waitPromise = navigationApi.waitForPanel('generate', 'settings', 200);
+ const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID, 200);
await expect(waitPromise).rejects.toThrow('Panel generate:settings registration timed out after 200ms');
@@ -295,9 +303,9 @@ describe('AppNavigationApi', () => {
const mockPanel2 = createMockPanel();
const mockPanel3 = createMockPanel();
- navigationApi.registerPanel('generate', 'settings', mockPanel1);
- navigationApi.registerPanel('generate', 'launchpad', mockPanel2);
- navigationApi.registerPanel('canvas', 'settings', mockPanel3);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
+ navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2);
+ navigationApi.registerPanel('canvas', SETTINGS_PANEL_ID, mockPanel3);
expect(navigationApi.getRegisteredPanels('generate')).toHaveLength(2);
expect(navigationApi.getRegisteredPanels('canvas')).toHaveLength(1);
@@ -309,7 +317,7 @@ describe('AppNavigationApi', () => {
});
it('should clean up pending promises when unregistering tab', async () => {
- const waitPromise = navigationApi.waitForPanel('generate', 'settings', 5000);
+ const waitPromise = navigationApi.waitForPanel('generate', SETTINGS_PANEL_ID, 5000);
navigationApi.unregisterTab('generate');
@@ -327,10 +335,10 @@ describe('AppNavigationApi', () => {
mockGetAppTab.mockReturnValue('canvas');
// Register panel
- const unregister = navigationApi.registerPanel('generate', 'settings', mockPanel);
+ const unregister = navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
// Focus panel (should switch tab and focus)
- const result = await navigationApi.focusPanel('generate', 'settings');
+ const result = await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
expect(result).toBe(true);
expect(mockSetAppTab).toHaveBeenCalledWith('generate');
@@ -342,7 +350,7 @@ describe('AppNavigationApi', () => {
expect(navigationApi.setAppTab).toBeNull();
expect(navigationApi.getAppTab).toBeNull();
- expect(navigationApi.isPanelRegistered('generate', 'settings')).toBe(false);
+ expect(navigationApi.isPanelRegistered('generate', SETTINGS_PANEL_ID)).toBe(false);
});
it('should handle multiple panels and tabs', async () => {
@@ -354,19 +362,19 @@ describe('AppNavigationApi', () => {
mockGetAppTab.mockReturnValue('generate');
// Register panels
- navigationApi.registerPanel('generate', 'settings', mockPanel1);
- navigationApi.registerPanel('generate', 'launchpad', mockPanel2);
- navigationApi.registerPanel('canvas', 'workspace', mockPanel3);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel1);
+ navigationApi.registerPanel('generate', LAUNCHPAD_PANEL_ID, mockPanel2);
+ navigationApi.registerPanel('canvas', WORKSPACE_PANEL_ID, mockPanel3);
// Focus panels
- await navigationApi.focusPanel('generate', 'settings');
+ await navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
expect(mockPanel1.api.setActive).toHaveBeenCalledOnce();
- await navigationApi.focusPanel('generate', 'launchpad');
+ await navigationApi.focusPanel('generate', LAUNCHPAD_PANEL_ID);
expect(mockPanel2.api.setActive).toHaveBeenCalledOnce();
mockGetAppTab.mockReturnValue('generate');
- await navigationApi.focusPanel('canvas', 'workspace');
+ await navigationApi.focusPanel('canvas', WORKSPACE_PANEL_ID);
expect(mockSetAppTab).toHaveBeenCalledWith('canvas');
expect(mockPanel3.api.setActive).toHaveBeenCalledOnce();
});
@@ -376,11 +384,11 @@ describe('AppNavigationApi', () => {
mockGetAppTab.mockReturnValue('generate');
// Start focusing before registration
- const focusPromise = navigationApi.focusPanel('generate', 'settings');
+ const focusPromise = navigationApi.focusPanel('generate', SETTINGS_PANEL_ID);
// Register after delay
setTimeout(() => {
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
}, 50);
const result = await focusPromise;
@@ -390,33 +398,6 @@ describe('AppNavigationApi', () => {
});
});
- describe('Tab Validation', () => {
- it('should reject non-enabled tabs in waitForPanel', async () => {
- await expect(navigationApi.waitForPanel('models', 'settings')).rejects.toThrow(
- 'Tab models is not enabled for panel registration'
- );
- });
-
- it('should reject non-enabled tabs in focusPanel', async () => {
- const result = await navigationApi.focusPanel('models', 'settings');
- expect(result).toBe(false);
- });
-
- it('should warn for non-enabled tabs in getPanel', () => {
- const result = navigationApi.getPanel('models', 'settings');
- expect(result).toBeUndefined();
- });
-
- it('should accept all enabled tabs', async () => {
- const enabledTabs = ['canvas', 'generate', 'workflows', 'upscaling'] as const;
-
- for (const tab of enabledTabs) {
- // These should timeout since panels aren't registered, but not reject due to invalid tab
- await expect(navigationApi.waitForPanel(tab, 'settings', 50)).rejects.toThrow('timed out');
- }
- });
- });
-
describe('focusPanelInActiveTab', () => {
beforeEach(() => {
navigationApi.connectToApp({ setAppTab: mockSetAppTab, getAppTab: mockGetAppTab });
@@ -424,10 +405,10 @@ describe('AppNavigationApi', () => {
it('should focus panel in active tab', async () => {
const mockPanel = createMockPanel();
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('generate');
- const result = await navigationApi.focusPanelInActiveTab('settings');
+ const result = await navigationApi.focusPanelInActiveTab(SETTINGS_PANEL_ID);
expect(result).toBe(true);
expect(mockPanel.api.setActive).toHaveBeenCalledOnce();
@@ -436,7 +417,7 @@ describe('AppNavigationApi', () => {
it('should return false when no active tab', async () => {
mockGetAppTab.mockReturnValue(null);
- const result = await navigationApi.focusPanelInActiveTab('settings');
+ const result = await navigationApi.focusPanelInActiveTab(SETTINGS_PANEL_ID);
expect(result).toBe(false);
});
@@ -444,7 +425,7 @@ describe('AppNavigationApi', () => {
it('should work without app connection', async () => {
navigationApi.disconnectFromApp();
- const result = await navigationApi.focusPanelInActiveTab('settings');
+ const result = await navigationApi.focusPanelInActiveTab(SETTINGS_PANEL_ID);
expect(result).toBe(false);
});
@@ -480,9 +461,9 @@ describe('AppNavigationApi', () => {
describe('getPanel', () => {
it('should return registered panel', () => {
const mockPanel = createMockPanel();
- navigationApi.registerPanel('generate', 'settings', mockPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, mockPanel);
- const result = navigationApi.getPanel('generate', 'settings');
+ const result = navigationApi.getPanel('generate', SETTINGS_PANEL_ID);
expect(result).toBe(mockPanel);
});
@@ -494,7 +475,7 @@ describe('AppNavigationApi', () => {
});
it('should return undefined for non-enabled tab', () => {
- const result = navigationApi.getPanel('models', 'settings');
+ const result = navigationApi.getPanel('models', SETTINGS_PANEL_ID);
expect(result).toBeUndefined();
});
@@ -507,7 +488,7 @@ describe('AppNavigationApi', () => {
it('should expand collapsed left panel', () => {
const mockPanel = createMockPanel({ maximumWidth: 0 });
- navigationApi.registerPanel('generate', 'left', mockPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleLeftPanel();
@@ -522,7 +503,7 @@ describe('AppNavigationApi', () => {
it('should collapse expanded left panel', () => {
const mockPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
- navigationApi.registerPanel('generate', 'left', mockPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleLeftPanel();
@@ -553,7 +534,7 @@ describe('AppNavigationApi', () => {
it('should return false when panel is not GridviewPanel', () => {
const mockPanel = createMockDockPanel();
- navigationApi.registerPanel('generate', 'left', mockPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleLeftPanel();
@@ -569,7 +550,7 @@ describe('AppNavigationApi', () => {
it('should expand collapsed right panel', () => {
const mockPanel = createMockPanel({ maximumWidth: 0 });
- navigationApi.registerPanel('generate', 'right', mockPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleRightPanel();
@@ -584,7 +565,7 @@ describe('AppNavigationApi', () => {
it('should collapse expanded right panel', () => {
const mockPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
- navigationApi.registerPanel('generate', 'right', mockPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleRightPanel();
@@ -615,7 +596,7 @@ describe('AppNavigationApi', () => {
it('should return false when panel is not GridviewPanel', () => {
const mockPanel = createMockDockPanel();
- navigationApi.registerPanel('generate', 'right', mockPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, mockPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleRightPanel();
@@ -633,8 +614,8 @@ describe('AppNavigationApi', () => {
const leftPanel = createMockPanel({ maximumWidth: 0 });
const rightPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
- navigationApi.registerPanel('generate', 'left', leftPanel);
- navigationApi.registerPanel('generate', 'right', rightPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleLeftAndRightPanels();
@@ -656,8 +637,8 @@ describe('AppNavigationApi', () => {
const leftPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
const rightPanel = createMockPanel({ maximumWidth: 0 });
- navigationApi.registerPanel('generate', 'left', leftPanel);
- navigationApi.registerPanel('generate', 'right', rightPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleLeftAndRightPanels();
@@ -679,8 +660,8 @@ describe('AppNavigationApi', () => {
const leftPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
const rightPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
- navigationApi.registerPanel('generate', 'left', leftPanel);
- navigationApi.registerPanel('generate', 'right', rightPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleLeftAndRightPanels();
@@ -702,8 +683,8 @@ describe('AppNavigationApi', () => {
const leftPanel = createMockPanel({ maximumWidth: 0 });
const rightPanel = createMockPanel({ maximumWidth: 0 });
- navigationApi.registerPanel('generate', 'left', leftPanel);
- navigationApi.registerPanel('generate', 'right', rightPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleLeftAndRightPanels();
@@ -741,8 +722,8 @@ describe('AppNavigationApi', () => {
const leftPanel = createMockDockPanel();
const rightPanel = createMockDockPanel();
- navigationApi.registerPanel('generate', 'left', leftPanel);
- navigationApi.registerPanel('generate', 'right', rightPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.toggleLeftAndRightPanels();
@@ -760,8 +741,8 @@ describe('AppNavigationApi', () => {
const leftPanel = createMockPanel({ maximumWidth: 0 });
const rightPanel = createMockPanel({ maximumWidth: 0 });
- navigationApi.registerPanel('generate', 'left', leftPanel);
- navigationApi.registerPanel('generate', 'right', rightPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.resetLeftAndRightPanels();
@@ -802,8 +783,8 @@ describe('AppNavigationApi', () => {
const leftPanel = createMockDockPanel();
const rightPanel = createMockDockPanel();
- navigationApi.registerPanel('generate', 'left', leftPanel);
- navigationApi.registerPanel('generate', 'right', rightPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
mockGetAppTab.mockReturnValue('generate');
const result = navigationApi.resetLeftAndRightPanels();
@@ -823,13 +804,13 @@ describe('AppNavigationApi', () => {
const settingsPanel = createMockPanel();
// Register panels
- navigationApi.registerPanel('generate', 'left', leftPanel);
- navigationApi.registerPanel('generate', 'right', rightPanel);
- navigationApi.registerPanel('generate', 'settings', settingsPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, leftPanel);
+ navigationApi.registerPanel('generate', RIGHT_PANEL_ID, rightPanel);
+ navigationApi.registerPanel('generate', SETTINGS_PANEL_ID, settingsPanel);
mockGetAppTab.mockReturnValue('generate');
// Focus a panel in active tab
- const focusResult = await navigationApi.focusPanelInActiveTab('settings');
+ const focusResult = await navigationApi.focusPanelInActiveTab(SETTINGS_PANEL_ID);
expect(focusResult).toBe(true);
expect(settingsPanel.api.setActive).toHaveBeenCalled();
@@ -854,8 +835,8 @@ describe('AppNavigationApi', () => {
const generateLeftPanel = createMockPanel({ maximumWidth: Number.MAX_SAFE_INTEGER });
const canvasLeftPanel = createMockPanel({ maximumWidth: 0 });
- navigationApi.registerPanel('generate', 'left', generateLeftPanel);
- navigationApi.registerPanel('canvas', 'left', canvasLeftPanel);
+ navigationApi.registerPanel('generate', LEFT_PANEL_ID, generateLeftPanel);
+ navigationApi.registerPanel('canvas', LEFT_PANEL_ID, canvasLeftPanel);
// Start on generate tab
mockGetAppTab.mockReturnValue('generate');
diff --git a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts
index 2cc6aedfb2..6303b10cb6 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts
+++ b/invokeai/frontend/web/src/features/ui/layouts/navigation-api.ts
@@ -3,7 +3,7 @@ import { createDeferredPromise, type Deferred } from 'common/util/createDeferred
import { GridviewPanel, type IDockviewPanel, type IGridviewPanel } from 'dockview';
import type { TabName } from 'features/ui/store/uiTypes';
-import { LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_MIN_SIZE_PX } from './shared';
+import { LEFT_PANEL_ID, LEFT_PANEL_MIN_SIZE_PX, RIGHT_PANEL_ID, RIGHT_PANEL_MIN_SIZE_PX } from './shared';
const log = logger('system');
@@ -14,8 +14,6 @@ type Waiter = {
timeoutId: ReturnType | null;
};
-const PANEL_ENABLED_TABS: TabName[] = ['canvas', 'generate', 'workflows', 'upscaling'];
-
export class NavigationApi {
private panels: Map = new Map();
private waiters: Map = new Map();
@@ -75,11 +73,6 @@ export class NavigationApi {
* @returns Promise that resolves when the panel is ready
*/
waitForPanel = (tab: TabName, panelId: string, timeout = 2000): Promise => {
- if (!PANEL_ENABLED_TABS.includes(tab)) {
- log.error(`Tab ${tab} is not enabled for panel registration`);
- return Promise.reject(new Error(`Tab ${tab} is not enabled for panel registration`));
- }
-
const key = this.getPanelKey(tab, panelId);
if (this.panels.has(key)) {
@@ -123,11 +116,6 @@ export class NavigationApi {
* @returns Promise that resolves to true if successful, false otherwise
*/
focusPanel = async (tab: TabName, panelId: string): Promise => {
- if (!PANEL_ENABLED_TABS.includes(tab)) {
- log.error(`Tab ${tab} is not enabled for panel registration`);
- return Promise.resolve(false);
- }
-
try {
// Switch to the target tab if needed
if (this.setAppTab && this.getAppTab && this.getAppTab() !== tab) {
@@ -176,10 +164,6 @@ export class NavigationApi {
};
getPanel = (tab: TabName, panelId: string): PanelType | undefined => {
- if (!PANEL_ENABLED_TABS.includes(tab)) {
- log.warn(`Tab ${tab} is not enabled for panel registration`);
- return undefined;
- }
const key = this.getPanelKey(tab, panelId);
return this.panels.get(key);
};
@@ -190,7 +174,7 @@ export class NavigationApi {
log.warn('No active tab found to toggle left panel');
return false;
}
- const leftPanel = this.getPanel(activeTab, 'left');
+ const leftPanel = this.getPanel(activeTab, LEFT_PANEL_ID);
if (!leftPanel) {
log.warn(`Left panel not found in active tab "${activeTab}"`);
return false;
@@ -216,7 +200,7 @@ export class NavigationApi {
log.warn('No active tab found to toggle right panel');
return false;
}
- const rightPanel = this.getPanel(activeTab, 'right');
+ const rightPanel = this.getPanel(activeTab, RIGHT_PANEL_ID);
if (!rightPanel) {
log.warn(`Right panel not found in active tab "${activeTab}"`);
return false;
@@ -242,8 +226,8 @@ export class NavigationApi {
log.warn('No active tab found to toggle right panel');
return false;
}
- const leftPanel = this.getPanel(activeTab, 'left');
- const rightPanel = this.getPanel(activeTab, 'right');
+ const leftPanel = this.getPanel(activeTab, LEFT_PANEL_ID);
+ const rightPanel = this.getPanel(activeTab, RIGHT_PANEL_ID);
if (!rightPanel || !leftPanel) {
log.warn(`Right and/or left panel not found in tab "${activeTab}"`);
@@ -277,8 +261,8 @@ export class NavigationApi {
log.warn('No active tab found to toggle right panel');
return false;
}
- const leftPanel = this.getPanel(activeTab, 'left');
- const rightPanel = this.getPanel(activeTab, 'right');
+ const leftPanel = this.getPanel(activeTab, LEFT_PANEL_ID);
+ const rightPanel = this.getPanel(activeTab, RIGHT_PANEL_ID);
if (!rightPanel || !leftPanel) {
log.warn(`Right and/or left panel not found in tab "${activeTab}"`);
diff --git a/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx b/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx
new file mode 100644
index 0000000000..7820aee9ab
--- /dev/null
+++ b/invokeai/frontend/web/src/features/ui/layouts/queue-tab-auto-layout.tsx
@@ -0,0 +1,64 @@
+import type { GridviewApi, IGridviewPanel, IGridviewReactProps } from 'dockview';
+import { GridviewReact, LayoutPriority, Orientation } from 'dockview';
+import QueueTab from 'features/ui/components/tabs/QueueTab';
+import type { RootLayoutGridviewComponents } from 'features/ui/layouts/auto-layout-context';
+import { AutoLayoutProvider } from 'features/ui/layouts/auto-layout-context';
+import { memo, useCallback, useEffect, useRef, useState } from 'react';
+
+import { navigationApi } from './navigation-api';
+import { QUEUE_PANEL_ID } from './shared';
+
+export const rootPanelComponents: RootLayoutGridviewComponents = {
+ [QUEUE_PANEL_ID]: QueueTab,
+};
+
+export const initializeRootPanelLayout = (layoutApi: GridviewApi) => {
+ const queue = layoutApi.addPanel({
+ id: QUEUE_PANEL_ID,
+ component: QUEUE_PANEL_ID,
+ priority: LayoutPriority.High,
+ });
+
+ navigationApi.registerPanel('queue', QUEUE_PANEL_ID, queue);
+
+ return { queue } satisfies Record;
+};
+
+export const QueueTabAutoLayout = memo(({ setIsLoading }: { setIsLoading: (isLoading: boolean) => void }) => {
+ const rootRef = useRef(null);
+ const [rootApi, setRootApi] = useState(null);
+ const onReady = useCallback(({ api }) => {
+ setRootApi(api);
+ }, []);
+
+ useEffect(() => {
+ setIsLoading(true);
+
+ if (!rootApi) {
+ return;
+ }
+
+ initializeRootPanelLayout(rootApi);
+
+ setTimeout(() => {
+ setIsLoading(false);
+ }, 300);
+
+ return () => {
+ navigationApi.unregisterTab('queue');
+ };
+ }, [rootApi, setIsLoading]);
+
+ return (
+
+
+
+ );
+});
+QueueTabAutoLayout.displayName = 'QueueTabAutoLayout';
diff --git a/invokeai/frontend/web/src/features/ui/layouts/shared.ts b/invokeai/frontend/web/src/features/ui/layouts/shared.ts
index 60dbb2e041..5dd836e37c 100644
--- a/invokeai/frontend/web/src/features/ui/layouts/shared.ts
+++ b/invokeai/frontend/web/src/features/ui/layouts/shared.ts
@@ -13,6 +13,9 @@ export const LAYERS_PANEL_ID = 'layers';
export const SETTINGS_PANEL_ID = 'settings';
+export const MODELS_PANEL_ID = 'models';
+export const QUEUE_PANEL_ID = 'queue';
+
export const DEFAULT_TAB_ID = 'default-tab';
export const TAB_WITH_PROGRESS_INDICATOR_ID = 'tab-with-progress-indicator';
export const TAB_WITH_LAUNCHPAD_ICON_ID = 'tab-with-launchpad-icon';