diff --git a/browser_tests/tests/bottomPanelShortcuts.spec.ts b/browser_tests/tests/bottomPanelShortcuts.spec.ts
new file mode 100644
index 0000000000..67a3670c53
--- /dev/null
+++ b/browser_tests/tests/bottomPanelShortcuts.spec.ts
@@ -0,0 +1,280 @@
+import { expect } from '@playwright/test'
+
+import { comfyPageFixture as test } from '../fixtures/ComfyPage'
+
+test.describe('Bottom Panel Shortcuts', () => {
+ test.beforeEach(async ({ comfyPage }) => {
+ await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
+ })
+
+ test('should toggle shortcuts panel visibility', async ({ comfyPage }) => {
+ // Initially shortcuts panel should be hidden
+ await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
+
+ // Click shortcuts toggle button in sidebar
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Shortcuts panel should now be visible
+ await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
+
+ // Click toggle button again to hide
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Panel should be hidden again
+ await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
+ })
+
+ test('should display essentials shortcuts tab', async ({ comfyPage }) => {
+ // Open shortcuts panel
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Essentials tab should be visible and active by default
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /Essential/i })
+ ).toBeVisible()
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /Essential/i })
+ ).toHaveAttribute('aria-selected', 'true')
+
+ // Should display shortcut categories
+ await expect(
+ comfyPage.page.locator('.subcategory-title').first()
+ ).toBeVisible()
+
+ // Should display some keyboard shortcuts
+ await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible()
+
+ // Should have workflow, node, and queue sections
+ await expect(
+ comfyPage.page.getByRole('heading', { name: 'Workflow' })
+ ).toBeVisible()
+ await expect(
+ comfyPage.page.getByRole('heading', { name: 'Node' })
+ ).toBeVisible()
+ await expect(
+ comfyPage.page.getByRole('heading', { name: 'Queue' })
+ ).toBeVisible()
+ })
+
+ test('should display view controls shortcuts tab', async ({ comfyPage }) => {
+ // Open shortcuts panel
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Click view controls tab
+ await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click()
+
+ // View controls tab should be active
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /View Controls/i })
+ ).toHaveAttribute('aria-selected', 'true')
+
+ // Should display view controls shortcuts
+ await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible()
+
+ // Should have view and panel controls sections
+ await expect(
+ comfyPage.page.getByRole('heading', { name: 'View' })
+ ).toBeVisible()
+ await expect(
+ comfyPage.page.getByRole('heading', { name: 'Panel Controls' })
+ ).toBeVisible()
+ })
+
+ test('should switch between shortcuts tabs', async ({ comfyPage }) => {
+ // Open shortcuts panel
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Essentials should be active initially
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /Essential/i })
+ ).toHaveAttribute('aria-selected', 'true')
+
+ // Click view controls tab
+ await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click()
+
+ // View controls should now be active
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /View Controls/i })
+ ).toHaveAttribute('aria-selected', 'true')
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /Essential/i })
+ ).not.toHaveAttribute('aria-selected', 'true')
+
+ // Switch back to essentials
+ await comfyPage.page.getByRole('tab', { name: /Essential/i }).click()
+
+ // Essentials should be active again
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /Essential/i })
+ ).toHaveAttribute('aria-selected', 'true')
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /View Controls/i })
+ ).not.toHaveAttribute('aria-selected', 'true')
+ })
+
+ test('should display formatted keyboard shortcuts', async ({ comfyPage }) => {
+ // Open shortcuts panel
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Wait for shortcuts to load
+ await comfyPage.page.waitForSelector('.key-badge')
+
+ // Check for common formatted keys
+ const keyBadges = comfyPage.page.locator('.key-badge')
+ const count = await keyBadges.count()
+ expect(count).toBeGreaterThanOrEqual(1)
+
+ // Should show formatted modifier keys
+ const badgeText = await keyBadges.allTextContents()
+ const hasModifiers = badgeText.some((text) =>
+ ['Ctrl', 'Cmd', 'Shift', 'Alt'].includes(text)
+ )
+ expect(hasModifiers).toBeTruthy()
+ })
+
+ test('should maintain panel state when switching to terminal', async ({
+ comfyPage
+ }) => {
+ // Open shortcuts panel first
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+ await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
+
+ // Open terminal panel (should switch panels)
+ await comfyPage.page
+ .locator('button[aria-label*="Toggle Bottom Panel"]')
+ .click()
+
+ // Panel should still be visible but showing terminal content
+ await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
+
+ // Switch back to shortcuts
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Should show shortcuts content again
+ await expect(
+ comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
+ ).toBeVisible()
+ })
+
+ test('should handle keyboard navigation', async ({ comfyPage }) => {
+ // Open shortcuts panel
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Focus the first tab
+ await comfyPage.page.getByRole('tab', { name: /Essential/i }).focus()
+
+ // Use arrow keys to navigate between tabs
+ await comfyPage.page.keyboard.press('ArrowRight')
+
+ // View controls tab should now have focus
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /View Controls/i })
+ ).toBeFocused()
+
+ // Press Enter to activate the tab
+ await comfyPage.page.keyboard.press('Enter')
+
+ // Tab should be selected
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /View Controls/i })
+ ).toHaveAttribute('aria-selected', 'true')
+ })
+
+ test('should close panel by clicking shortcuts button again', async ({
+ comfyPage
+ }) => {
+ // Open shortcuts panel
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+ await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
+
+ // Click shortcuts button again to close
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Panel should be hidden
+ await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
+ })
+
+ test('should display shortcuts in organized columns', async ({
+ comfyPage
+ }) => {
+ // Open shortcuts panel
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Should have 3-column grid layout
+ await expect(comfyPage.page.locator('.md\\:grid-cols-3')).toBeVisible()
+
+ // Should have multiple subcategory sections
+ const subcategoryTitles = comfyPage.page.locator('.subcategory-title')
+ const titleCount = await subcategoryTitles.count()
+ expect(titleCount).toBeGreaterThanOrEqual(2)
+ })
+
+ test('should open shortcuts panel with Ctrl+Shift+K', async ({
+ comfyPage
+ }) => {
+ // Initially shortcuts panel should be hidden
+ await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
+
+ // Press Ctrl+Shift+K to open shortcuts panel
+ await comfyPage.page.keyboard.press('Control+Shift+KeyK')
+
+ // Shortcuts panel should now be visible
+ await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
+
+ // Should show essentials tab by default
+ await expect(
+ comfyPage.page.getByRole('tab', { name: /Essential/i })
+ ).toHaveAttribute('aria-selected', 'true')
+ })
+
+ test('should open settings dialog when clicking manage shortcuts button', async ({
+ comfyPage
+ }) => {
+ // Open shortcuts panel
+ await comfyPage.page
+ .locator('button[aria-label*="Keyboard Shortcuts"]')
+ .click()
+
+ // Manage shortcuts button should be visible
+ await expect(
+ comfyPage.page.getByRole('button', { name: /Manage Shortcuts/i })
+ ).toBeVisible()
+
+ // Click manage shortcuts button
+ await comfyPage.page
+ .getByRole('button', { name: /Manage Shortcuts/i })
+ .click()
+
+ // Settings dialog should open with keybinding tab
+ await expect(comfyPage.page.getByRole('dialog')).toBeVisible()
+
+ // Should show keybinding settings (check for keybinding-related content)
+ await expect(
+ comfyPage.page.getByRole('option', { name: 'Keybinding' })
+ ).toBeVisible()
+ })
+})
diff --git a/src/components/bottomPanel/BottomPanel.vue b/src/components/bottomPanel/BottomPanel.vue
index a492687538..63d6874c03 100644
--- a/src/components/bottomPanel/BottomPanel.vue
+++ b/src/components/bottomPanel/BottomPanel.vue
@@ -11,18 +11,33 @@
class="p-3 border-none"
>
- {{ tab.title.toUpperCase() }}
+ {{
+ shouldCapitalizeTab(tab.id)
+ ? tab.title.toUpperCase()
+ : tab.title
+ }}
-
+
+
+
+
@@ -44,9 +59,32 @@ import Button from 'primevue/button'
import Tab from 'primevue/tab'
import TabList from 'primevue/tablist'
import Tabs from 'primevue/tabs'
+import { computed } from 'vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
+import { useDialogService } from '@/services/dialogService'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
const bottomPanelStore = useBottomPanelStore()
+const dialogService = useDialogService()
+
+const isShortcutsTabActive = computed(() => {
+ const activeTabId = bottomPanelStore.activeBottomPanelTabId
+ return (
+ activeTabId === 'shortcuts-essentials' ||
+ activeTabId === 'shortcuts-view-controls'
+ )
+})
+
+const shouldCapitalizeTab = (tabId: string): boolean => {
+ return tabId !== 'shortcuts-essentials' && tabId !== 'shortcuts-view-controls'
+}
+
+const openKeybindingSettings = async () => {
+ dialogService.showSettingsDialog('keybinding')
+}
+
+const closeBottomPanel = () => {
+ bottomPanelStore.activePanel = null
+}
diff --git a/src/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue b/src/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue
new file mode 100644
index 0000000000..459d095ae2
--- /dev/null
+++ b/src/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue b/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue
new file mode 100644
index 0000000000..10f3c7b59e
--- /dev/null
+++ b/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue
@@ -0,0 +1,119 @@
+
+
+
+
+
+ {{ getSubcategoryTitle(subcategory) }}
+
+
+
+
+
+
+ {{ command.label || command.id }}
+
+
+
+
+
+
+ {{ formatKey(key) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/bottomPanel/tabs/shortcuts/ViewControlsPanel.vue b/src/components/bottomPanel/tabs/shortcuts/ViewControlsPanel.vue
new file mode 100644
index 0000000000..4852e9d0ac
--- /dev/null
+++ b/src/components/bottomPanel/tabs/shortcuts/ViewControlsPanel.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
diff --git a/src/components/graph/MiniMap.vue b/src/components/graph/MiniMap.vue
index 3b1a3c9536..2e0fb1a3c1 100644
--- a/src/components/graph/MiniMap.vue
+++ b/src/components/graph/MiniMap.vue
@@ -2,7 +2,11 @@
+
@@ -32,6 +33,7 @@ import { computed } from 'vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
+import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useSettingStore } from '@/stores/settingStore'
import { useUserStore } from '@/stores/userStore'
diff --git a/src/components/sidebar/SidebarBottomPanelToggleButton.vue b/src/components/sidebar/SidebarBottomPanelToggleButton.vue
index 6ca891e6e9..c51d9df89a 100644
--- a/src/components/sidebar/SidebarBottomPanelToggleButton.vue
+++ b/src/components/sidebar/SidebarBottomPanelToggleButton.vue
@@ -1,7 +1,7 @@
diff --git a/src/components/sidebar/SidebarShortcutsToggleButton.vue b/src/components/sidebar/SidebarShortcutsToggleButton.vue
new file mode 100644
index 0000000000..f2e4ec400e
--- /dev/null
+++ b/src/components/sidebar/SidebarShortcutsToggleButton.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/bottomPanelTabs/useCommandSubcategories.ts b/src/composables/bottomPanelTabs/useCommandSubcategories.ts
new file mode 100644
index 0000000000..18131b0336
--- /dev/null
+++ b/src/composables/bottomPanelTabs/useCommandSubcategories.ts
@@ -0,0 +1,78 @@
+import { type ComputedRef, computed } from 'vue'
+
+import { type ComfyCommandImpl } from '@/stores/commandStore'
+
+export type SubcategoryRule = {
+ pattern: string | RegExp
+ subcategory: string
+}
+
+export type SubcategoryConfig = {
+ defaultSubcategory: string
+ rules: SubcategoryRule[]
+}
+
+/**
+ * Composable for grouping commands by subcategory based on configurable rules
+ */
+export function useCommandSubcategories(
+ commands: ComputedRef,
+ config: SubcategoryConfig
+) {
+ const subcategories = computed(() => {
+ const result: Record = {}
+
+ for (const command of commands.value) {
+ let subcategory = config.defaultSubcategory
+
+ // Find the first matching rule
+ for (const rule of config.rules) {
+ const matches =
+ typeof rule.pattern === 'string'
+ ? command.id.includes(rule.pattern)
+ : rule.pattern.test(command.id)
+
+ if (matches) {
+ subcategory = rule.subcategory
+ break
+ }
+ }
+
+ if (!result[subcategory]) {
+ result[subcategory] = []
+ }
+ result[subcategory].push(command)
+ }
+
+ return result
+ })
+
+ return {
+ subcategories
+ }
+}
+
+/**
+ * Predefined configuration for view controls subcategories
+ */
+export const VIEW_CONTROLS_CONFIG: SubcategoryConfig = {
+ defaultSubcategory: 'view',
+ rules: [
+ { pattern: 'Zoom', subcategory: 'view' },
+ { pattern: 'Fit', subcategory: 'view' },
+ { pattern: 'Panel', subcategory: 'panel-controls' },
+ { pattern: 'Sidebar', subcategory: 'panel-controls' }
+ ]
+}
+
+/**
+ * Predefined configuration for essentials subcategories
+ */
+export const ESSENTIALS_CONFIG: SubcategoryConfig = {
+ defaultSubcategory: 'workflow',
+ rules: [
+ { pattern: 'Workflow', subcategory: 'workflow' },
+ { pattern: 'Node', subcategory: 'node' },
+ { pattern: 'Queue', subcategory: 'queue' }
+ ]
+}
diff --git a/src/composables/bottomPanelTabs/useShortcutsTab.ts b/src/composables/bottomPanelTabs/useShortcutsTab.ts
new file mode 100644
index 0000000000..e06cf933d3
--- /dev/null
+++ b/src/composables/bottomPanelTabs/useShortcutsTab.ts
@@ -0,0 +1,27 @@
+import { markRaw } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+import EssentialsPanel from '@/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue'
+import ViewControlsPanel from '@/components/bottomPanel/tabs/shortcuts/ViewControlsPanel.vue'
+import { BottomPanelExtension } from '@/types/extensionTypes'
+
+export const useShortcutsTab = (): BottomPanelExtension[] => {
+ const { t } = useI18n()
+
+ return [
+ {
+ id: 'shortcuts-essentials',
+ title: t('shortcuts.essentials'),
+ component: markRaw(EssentialsPanel),
+ type: 'vue',
+ targetPanel: 'shortcuts'
+ },
+ {
+ id: 'shortcuts-view-controls',
+ title: t('shortcuts.viewControls'),
+ component: markRaw(ViewControlsPanel),
+ type: 'vue',
+ targetPanel: 'shortcuts'
+ }
+ ]
+}
diff --git a/src/composables/useCoreCommands.ts b/src/composables/useCoreCommands.ts
index 9752321f93..6f71bc402f 100644
--- a/src/composables/useCoreCommands.ts
+++ b/src/composables/useCoreCommands.ts
@@ -46,6 +46,9 @@ export function useCoreCommands(): ComfyCommand[] {
const toastStore = useToastStore()
const canvasStore = useCanvasStore()
const executionStore = useExecutionStore()
+
+ const bottomPanelStore = useBottomPanelStore()
+
const { getSelectedNodes, toggleSelectedNodesMode } =
useSelectedLiteGraphItems()
const getTracker = () => workflowStore.activeWorkflow?.changeTracker
@@ -70,6 +73,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-plus',
label: 'New Blank Workflow',
menubarLabel: 'New',
+ category: 'essentials' as const,
function: () => workflowService.loadBlankWorkflow()
},
{
@@ -77,6 +81,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-folder-open',
label: 'Open Workflow',
menubarLabel: 'Open',
+ category: 'essentials' as const,
function: () => {
app.ui.loadFile()
}
@@ -92,6 +97,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-save',
label: 'Save Workflow',
menubarLabel: 'Save',
+ category: 'essentials' as const,
function: async () => {
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
if (!workflow) return
@@ -104,6 +110,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-save',
label: 'Save Workflow As',
menubarLabel: 'Save As',
+ category: 'essentials' as const,
function: async () => {
const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow
if (!workflow) return
@@ -116,6 +123,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-download',
label: 'Export Workflow',
menubarLabel: 'Export',
+ category: 'essentials' as const,
function: async () => {
await workflowService.exportWorkflow('workflow', 'workflow')
}
@@ -133,6 +141,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Undo',
icon: 'pi pi-undo',
label: 'Undo',
+ category: 'essentials' as const,
function: async () => {
await getTracker()?.undo?.()
}
@@ -141,6 +150,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Redo',
icon: 'pi pi-refresh',
label: 'Redo',
+ category: 'essentials' as const,
function: async () => {
await getTracker()?.redo?.()
}
@@ -149,6 +159,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.ClearWorkflow',
icon: 'pi pi-trash',
label: 'Clear Workflow',
+ category: 'essentials' as const,
function: () => {
const settingStore = useSettingStore()
if (
@@ -190,6 +201,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.RefreshNodeDefinitions',
icon: 'pi pi-refresh',
label: 'Refresh Node Definitions',
+ category: 'essentials' as const,
function: async () => {
await app.refreshComboInNodes()
}
@@ -198,6 +210,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Interrupt',
icon: 'pi pi-stop',
label: 'Interrupt',
+ category: 'essentials' as const,
function: async () => {
await api.interrupt(executionStore.activePromptId)
toastStore.add({
@@ -212,6 +225,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.ClearPendingTasks',
icon: 'pi pi-stop',
label: 'Clear Pending Tasks',
+ category: 'essentials' as const,
function: async () => {
await useQueueStore().clear(['queue'])
toastStore.add({
@@ -234,6 +248,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.ZoomIn',
icon: 'pi pi-plus',
label: 'Zoom In',
+ category: 'view-controls' as const,
function: () => {
const ds = app.canvas.ds
ds.changeScale(
@@ -247,6 +262,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.ZoomOut',
icon: 'pi pi-minus',
label: 'Zoom Out',
+ category: 'view-controls' as const,
function: () => {
const ds = app.canvas.ds
ds.changeScale(
@@ -260,6 +276,7 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.FitView',
icon: 'pi pi-expand',
label: 'Fit view to selected nodes',
+ category: 'view-controls' as const,
function: () => {
if (app.canvas.empty) {
toastStore.add({
@@ -325,6 +342,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-play',
label: 'Queue Prompt',
versionAdded: '1.3.7',
+ category: 'essentials' as const,
function: async () => {
const batchCount = useQueueSettingsStore().batchCount
await app.queuePrompt(0, batchCount)
@@ -335,6 +353,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-play',
label: 'Queue Prompt (Front)',
versionAdded: '1.3.7',
+ category: 'essentials' as const,
function: async () => {
const batchCount = useQueueSettingsStore().batchCount
await app.queuePrompt(-1, batchCount)
@@ -371,6 +390,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-cog',
label: 'Show Settings Dialog',
versionAdded: '1.3.7',
+ category: 'view-controls' as const,
function: () => {
dialogService.showSettingsDialog()
}
@@ -380,6 +400,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-sitemap',
label: 'Group Selected Nodes',
versionAdded: '1.3.7',
+ category: 'essentials' as const,
function: () => {
const { canvas } = app
if (!canvas.selectedItems?.size) {
@@ -423,6 +444,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-volume-off',
label: 'Mute/Unmute Selected Nodes',
versionAdded: '1.3.11',
+ category: 'essentials' as const,
function: () => {
toggleSelectedNodesMode(LGraphEventMode.NEVER)
app.canvas.setDirty(true, true)
@@ -433,6 +455,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-shield',
label: 'Bypass/Unbypass Selected Nodes',
versionAdded: '1.3.11',
+ category: 'essentials' as const,
function: () => {
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
app.canvas.setDirty(true, true)
@@ -443,6 +466,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-pin',
label: 'Pin/Unpin Selected Nodes',
versionAdded: '1.3.11',
+ category: 'essentials' as const,
function: () => {
getSelectedNodes().forEach((node) => {
node.pin(!node.pinned)
@@ -516,8 +540,9 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-list',
label: 'Toggle Bottom Panel',
versionAdded: '1.3.22',
+ category: 'view-controls' as const,
function: () => {
- useBottomPanelStore().toggleBottomPanel()
+ bottomPanelStore.toggleBottomPanel()
}
},
{
@@ -525,6 +550,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-eye',
label: 'Toggle Focus Mode',
versionAdded: '1.3.27',
+ category: 'view-controls' as const,
function: () => {
useWorkspaceStore().toggleFocusMode()
}
@@ -750,6 +776,7 @@ export function useCoreCommands(): ComfyCommand[] {
icon: 'pi pi-sitemap',
label: 'Convert Selection to Subgraph',
versionAdded: '1.20.1',
+ category: 'essentials' as const,
function: () => {
const canvas = canvasStore.getCanvas()
const graph = canvas.subgraph ?? canvas.graph
@@ -768,6 +795,16 @@ export function useCoreCommands(): ComfyCommand[] {
const { node } = res
canvas.select(node)
}
+ },
+ {
+ id: 'Workspace.ToggleBottomPanel.Shortcuts',
+ icon: 'pi pi-key',
+ label: 'Show Keybindings Dialog',
+ versionAdded: '1.24.1',
+ category: 'view-controls' as const,
+ function: () => {
+ bottomPanelStore.togglePanel('shortcuts')
+ }
}
]
diff --git a/src/constants/coreKeybindings.ts b/src/constants/coreKeybindings.ts
index c61fd0b357..2b60077247 100644
--- a/src/constants/coreKeybindings.ts
+++ b/src/constants/coreKeybindings.ts
@@ -182,5 +182,13 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
alt: true
},
commandId: 'Comfy.Canvas.ToggleMinimap'
+ },
+ {
+ combo: {
+ ctrl: true,
+ shift: true,
+ key: 'k'
+ },
+ commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
}
]
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 3d4d770a6b..2d4be43bfa 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -1630,5 +1630,19 @@
"clearWorkflow": "Clear Workflow",
"deleteWorkflow": "Delete Workflow",
"enterNewName": "Enter new name"
+ },
+ "shortcuts": {
+ "essentials": "Essential",
+ "viewControls": "View Controls",
+ "manageShortcuts": "Manage Shortcuts",
+ "noKeybinding": "No keybinding",
+ "keyboardShortcuts": "Keyboard Shortcuts",
+ "subcategories": {
+ "workflow": "Workflow",
+ "node": "Node",
+ "queue": "Queue",
+ "view": "View",
+ "panelControls": "Panel Controls"
+ }
}
}
\ No newline at end of file
diff --git a/src/stores/commandStore.ts b/src/stores/commandStore.ts
index 214296e572..70154e5aee 100644
--- a/src/stores/commandStore.ts
+++ b/src/stores/commandStore.ts
@@ -17,6 +17,7 @@ export interface ComfyCommand {
versionAdded?: string
confirmation?: string // If non-nullish, this command will prompt for confirmation
source?: string
+ category?: 'essentials' | 'view-controls' // For shortcuts panel organization
}
export class ComfyCommandImpl implements ComfyCommand {
@@ -29,6 +30,7 @@ export class ComfyCommandImpl implements ComfyCommand {
versionAdded?: string
confirmation?: string
source?: string
+ category?: 'essentials' | 'view-controls'
constructor(command: ComfyCommand) {
this.id = command.id
@@ -40,6 +42,7 @@ export class ComfyCommandImpl implements ComfyCommand {
this.versionAdded = command.versionAdded
this.confirmation = command.confirmation
this.source = command.source
+ this.category = command.category
}
get label() {
diff --git a/src/stores/workspace/bottomPanelStore.ts b/src/stores/workspace/bottomPanelStore.ts
index 9e1c923e4b..d2b96be951 100644
--- a/src/stores/workspace/bottomPanelStore.ts
+++ b/src/stores/workspace/bottomPanelStore.ts
@@ -1,6 +1,7 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
+import { useShortcutsTab } from '@/composables/bottomPanelTabs/useShortcutsTab'
import {
useCommandTerminalTab,
useLogsTerminalTab
@@ -10,45 +11,110 @@ import { ComfyExtension } from '@/types/comfy'
import type { BottomPanelExtension } from '@/types/extensionTypes'
import { isElectron } from '@/utils/envUtil'
+type PanelType = 'terminal' | 'shortcuts'
+
+interface PanelState {
+ tabs: BottomPanelExtension[]
+ activeTabId: string
+ visible: boolean
+}
+
export const useBottomPanelStore = defineStore('bottomPanel', () => {
- const bottomPanelVisible = ref(false)
- const toggleBottomPanel = () => {
- // If there are no tabs, don't show the bottom panel
- if (bottomPanelTabs.value.length === 0) {
- return
- }
- bottomPanelVisible.value = !bottomPanelVisible.value
- }
+ // Multi-panel state
+ const panels = ref>({
+ terminal: { tabs: [], activeTabId: '', visible: false },
+ shortcuts: { tabs: [], activeTabId: '', visible: false }
+ })
+
+ const activePanel = ref(null)
+
+ // Computed properties for active panel
+ const activePanelState = computed(() =>
+ activePanel.value ? panels.value[activePanel.value] : null
+ )
- const bottomPanelTabs = ref([])
- const activeBottomPanelTabId = ref('')
const activeBottomPanelTab = computed(() => {
- return (
- bottomPanelTabs.value.find(
- (tab) => tab.id === activeBottomPanelTabId.value
- ) ?? null
- )
+ const state = activePanelState.value
+ if (!state) return null
+ return state.tabs.find((tab) => tab.id === state.activeTabId) ?? null
+ })
+
+ const bottomPanelVisible = computed({
+ get: () => !!activePanel.value,
+ set: (visible: boolean) => {
+ if (!visible) {
+ activePanel.value = null
+ }
+ }
})
+ const bottomPanelTabs = computed(() => activePanelState.value?.tabs ?? [])
+ const activeBottomPanelTabId = computed({
+ get: () => activePanelState.value?.activeTabId ?? '',
+ set: (tabId: string) => {
+ const state = activePanelState.value
+ if (state) {
+ state.activeTabId = tabId
+ }
+ }
+ })
+
+ const togglePanel = (panelType: PanelType) => {
+ const panel = panels.value[panelType]
+ if (panel.tabs.length === 0) return
+
+ if (activePanel.value === panelType) {
+ // Hide current panel
+ activePanel.value = null
+ } else {
+ // Show target panel
+ activePanel.value = panelType
+ if (!panel.activeTabId && panel.tabs.length > 0) {
+ panel.activeTabId = panel.tabs[0].id
+ }
+ }
+ }
+
+ const toggleBottomPanel = () => {
+ // Legacy method - toggles terminal panel
+ togglePanel('terminal')
+ }
+
const setActiveTab = (tabId: string) => {
- activeBottomPanelTabId.value = tabId
+ const state = activePanelState.value
+ if (state) {
+ state.activeTabId = tabId
+ }
}
+
const toggleBottomPanelTab = (tabId: string) => {
- if (activeBottomPanelTabId.value === tabId && bottomPanelVisible.value) {
- bottomPanelVisible.value = false
- } else {
- activeBottomPanelTabId.value = tabId
- bottomPanelVisible.value = true
+ // Find which panel contains this tab
+ for (const [panelType, panel] of Object.entries(panels.value)) {
+ const tab = panel.tabs.find((t) => t.id === tabId)
+ if (tab) {
+ if (activePanel.value === panelType && panel.activeTabId === tabId) {
+ activePanel.value = null
+ } else {
+ activePanel.value = panelType as PanelType
+ panel.activeTabId = tabId
+ }
+ return
+ }
}
}
const registerBottomPanelTab = (tab: BottomPanelExtension) => {
- bottomPanelTabs.value = [...bottomPanelTabs.value, tab]
- if (bottomPanelTabs.value.length === 1) {
- activeBottomPanelTabId.value = tab.id
+ const targetPanel = tab.targetPanel ?? 'terminal'
+ const panel = panels.value[targetPanel]
+
+ panel.tabs = [...panel.tabs, tab]
+ if (panel.tabs.length === 1) {
+ panel.activeTabId = tab.id
}
+
useCommandStore().registerCommand({
id: `Workspace.ToggleBottomPanelTab.${tab.id}`,
icon: 'pi pi-list',
label: `Toggle ${tab.title} Bottom Panel`,
+ category: 'view-controls' as const,
function: () => toggleBottomPanelTab(tab.id),
source: 'System'
})
@@ -59,6 +125,7 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
if (isElectron()) {
registerBottomPanelTab(useCommandTerminalTab())
}
+ useShortcutsTab().forEach(registerBottomPanelTab)
}
const registerExtensionBottomPanelTabs = (extension: ComfyExtension) => {
@@ -68,6 +135,11 @@ export const useBottomPanelStore = defineStore('bottomPanel', () => {
}
return {
+ // Multi-panel API
+ panels,
+ activePanel,
+ togglePanel,
+
bottomPanelVisible,
toggleBottomPanel,
bottomPanelTabs,
diff --git a/src/stores/workspace/sidebarTabStore.ts b/src/stores/workspace/sidebarTabStore.ts
index c5b66c359a..876736619b 100644
--- a/src/stores/workspace/sidebarTabStore.ts
+++ b/src/stores/workspace/sidebarTabStore.ts
@@ -44,6 +44,7 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
label: labelFunction,
tooltip: tooltipFunction,
versionAdded: '1.3.9',
+ category: 'view-controls' as const,
function: () => {
toggleSidebarTab(tab.id)
},
diff --git a/src/types/extensionTypes.ts b/src/types/extensionTypes.ts
index 88e82c0e1a..e9d7051157 100644
--- a/src/types/extensionTypes.ts
+++ b/src/types/extensionTypes.ts
@@ -14,6 +14,7 @@ export interface BaseSidebarTabExtension {
export interface BaseBottomPanelExtension {
id: string
title: string
+ targetPanel?: 'terminal' | 'shortcuts'
}
export interface VueExtension {
diff --git a/tests-ui/tests/components/bottomPanel/EssentialsPanel.spec.ts b/tests-ui/tests/components/bottomPanel/EssentialsPanel.spec.ts
new file mode 100644
index 0000000000..d8ad4d6a05
--- /dev/null
+++ b/tests-ui/tests/components/bottomPanel/EssentialsPanel.spec.ts
@@ -0,0 +1,87 @@
+import { mount } from '@vue/test-utils'
+import { createPinia, setActivePinia } from 'pinia'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import EssentialsPanel from '@/components/bottomPanel/tabs/shortcuts/EssentialsPanel.vue'
+import ShortcutsList from '@/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue'
+import type { ComfyCommandImpl } from '@/stores/commandStore'
+
+// Mock ShortcutsList component
+vi.mock('@/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue', () => ({
+ default: {
+ name: 'ShortcutsList',
+ props: ['commands', 'subcategories', 'columns'],
+ template:
+ '{{ commands.length }} commands
'
+ }
+}))
+
+// Mock command store
+const mockCommands: ComfyCommandImpl[] = [
+ {
+ id: 'Workflow.New',
+ label: 'New Workflow',
+ category: 'essentials'
+ } as ComfyCommandImpl,
+ {
+ id: 'Node.Add',
+ label: 'Add Node',
+ category: 'essentials'
+ } as ComfyCommandImpl,
+ {
+ id: 'Queue.Clear',
+ label: 'Clear Queue',
+ category: 'essentials'
+ } as ComfyCommandImpl,
+ {
+ id: 'Other.Command',
+ label: 'Other Command',
+ category: 'view-controls',
+ function: vi.fn(),
+ icon: 'pi pi-test',
+ tooltip: 'Test tooltip',
+ menubarLabel: 'Other Command',
+ keybinding: null
+ } as ComfyCommandImpl
+]
+
+vi.mock('@/stores/commandStore', () => ({
+ useCommandStore: () => ({
+ commands: mockCommands
+ })
+}))
+
+describe('EssentialsPanel', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ })
+
+ it('should render ShortcutsList with essentials commands', () => {
+ const wrapper = mount(EssentialsPanel)
+
+ const shortcutsList = wrapper.findComponent(ShortcutsList)
+ expect(shortcutsList.exists()).toBe(true)
+
+ // Should pass only essentials commands
+ const commands = shortcutsList.props('commands')
+ expect(commands).toHaveLength(3)
+ commands.forEach((cmd: ComfyCommandImpl) => {
+ expect(cmd.category).toBe('essentials')
+ })
+ })
+
+ it('should categorize commands into subcategories', () => {
+ const wrapper = mount(EssentialsPanel)
+
+ const shortcutsList = wrapper.findComponent(ShortcutsList)
+ const subcategories = shortcutsList.props('subcategories')
+
+ expect(subcategories).toHaveProperty('workflow')
+ expect(subcategories).toHaveProperty('node')
+ expect(subcategories).toHaveProperty('queue')
+
+ expect(subcategories.workflow).toContain(mockCommands[0])
+ expect(subcategories.node).toContain(mockCommands[1])
+ expect(subcategories.queue).toContain(mockCommands[2])
+ })
+})
diff --git a/tests-ui/tests/components/bottomPanel/ShortcutsList.spec.ts b/tests-ui/tests/components/bottomPanel/ShortcutsList.spec.ts
new file mode 100644
index 0000000000..d42e7250c7
--- /dev/null
+++ b/tests-ui/tests/components/bottomPanel/ShortcutsList.spec.ts
@@ -0,0 +1,165 @@
+import { mount } from '@vue/test-utils'
+import { describe, expect, it, vi } from 'vitest'
+
+import ShortcutsList from '@/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue'
+import type { ComfyCommandImpl } from '@/stores/commandStore'
+
+// Mock vue-i18n
+const mockT = vi.fn((key: string) => {
+ const translations: Record = {
+ 'shortcuts.subcategories.workflow': 'Workflow',
+ 'shortcuts.subcategories.node': 'Node',
+ 'shortcuts.subcategories.queue': 'Queue',
+ 'shortcuts.subcategories.view': 'View',
+ 'shortcuts.subcategories.panelControls': 'Panel Controls'
+ }
+ return translations[key] || key
+})
+
+vi.mock('vue-i18n', () => ({
+ useI18n: () => ({
+ t: mockT
+ })
+}))
+
+describe('ShortcutsList', () => {
+ const mockCommands: ComfyCommandImpl[] = [
+ {
+ id: 'Workflow.New',
+ label: 'New Workflow',
+ category: 'essentials',
+ keybinding: {
+ combo: {
+ getKeySequences: () => ['Control', 'n']
+ }
+ }
+ } as ComfyCommandImpl,
+ {
+ id: 'Node.Add',
+ label: 'Add Node',
+ category: 'essentials',
+ keybinding: {
+ combo: {
+ getKeySequences: () => ['Shift', 'a']
+ }
+ }
+ } as ComfyCommandImpl,
+ {
+ id: 'Queue.Clear',
+ label: 'Clear Queue',
+ category: 'essentials',
+ keybinding: {
+ combo: {
+ getKeySequences: () => ['Control', 'Shift', 'c']
+ }
+ }
+ } as ComfyCommandImpl
+ ]
+
+ const mockSubcategories = {
+ workflow: [mockCommands[0]],
+ node: [mockCommands[1]],
+ queue: [mockCommands[2]]
+ }
+
+ it('should render shortcuts organized by subcategories', () => {
+ const wrapper = mount(ShortcutsList, {
+ props: {
+ commands: mockCommands,
+ subcategories: mockSubcategories
+ }
+ })
+
+ // Check that subcategories are rendered
+ expect(wrapper.text()).toContain('Workflow')
+ expect(wrapper.text()).toContain('Node')
+ expect(wrapper.text()).toContain('Queue')
+
+ // Check that commands are rendered
+ expect(wrapper.text()).toContain('New Workflow')
+ expect(wrapper.text()).toContain('Add Node')
+ expect(wrapper.text()).toContain('Clear Queue')
+ })
+
+ it('should format keyboard shortcuts correctly', () => {
+ const wrapper = mount(ShortcutsList, {
+ props: {
+ commands: mockCommands,
+ subcategories: mockSubcategories
+ }
+ })
+
+ // Check for formatted keys
+ expect(wrapper.text()).toContain('Ctrl')
+ expect(wrapper.text()).toContain('n')
+ expect(wrapper.text()).toContain('Shift')
+ expect(wrapper.text()).toContain('a')
+ expect(wrapper.text()).toContain('c')
+ })
+
+ it('should filter out commands without keybindings', () => {
+ const commandsWithoutKeybinding: ComfyCommandImpl[] = [
+ ...mockCommands,
+ {
+ id: 'No.Keybinding',
+ label: 'No Keybinding',
+ category: 'essentials',
+ keybinding: null
+ } as ComfyCommandImpl
+ ]
+
+ const wrapper = mount(ShortcutsList, {
+ props: {
+ commands: commandsWithoutKeybinding,
+ subcategories: {
+ ...mockSubcategories,
+ other: [commandsWithoutKeybinding[3]]
+ }
+ }
+ })
+
+ expect(wrapper.text()).not.toContain('No Keybinding')
+ })
+
+ it('should handle special key formatting', () => {
+ const specialKeyCommand: ComfyCommandImpl = {
+ id: 'Special.Keys',
+ label: 'Special Keys',
+ category: 'essentials',
+ keybinding: {
+ combo: {
+ getKeySequences: () => ['Meta', 'ArrowUp', 'Enter', 'Escape', ' ']
+ }
+ }
+ } as ComfyCommandImpl
+
+ const wrapper = mount(ShortcutsList, {
+ props: {
+ commands: [specialKeyCommand],
+ subcategories: {
+ special: [specialKeyCommand]
+ }
+ }
+ })
+
+ const text = wrapper.text()
+ expect(text).toContain('Cmd') // Meta -> Cmd
+ expect(text).toContain('↑') // ArrowUp -> ↑
+ expect(text).toContain('↵') // Enter -> ↵
+ expect(text).toContain('Esc') // Escape -> Esc
+ expect(text).toContain('Space') // ' ' -> Space
+ })
+
+ it('should use fallback subcategory titles', () => {
+ const wrapper = mount(ShortcutsList, {
+ props: {
+ commands: mockCommands,
+ subcategories: {
+ unknown: [mockCommands[0]]
+ }
+ }
+ })
+
+ expect(wrapper.text()).toContain('unknown')
+ })
+})
diff --git a/tests-ui/tests/store/bottomPanelStore.test.ts b/tests-ui/tests/store/bottomPanelStore.test.ts
new file mode 100644
index 0000000000..7d9a2406e2
--- /dev/null
+++ b/tests-ui/tests/store/bottomPanelStore.test.ts
@@ -0,0 +1,166 @@
+import { createPinia, setActivePinia } from 'pinia'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
+import type { BottomPanelExtension } from '@/types/extensionTypes'
+
+// Mock dependencies
+vi.mock('@/composables/bottomPanelTabs/useShortcutsTab', () => ({
+ useShortcutsTab: () => [
+ {
+ id: 'shortcuts-essentials',
+ title: 'Essentials',
+ component: {},
+ type: 'vue',
+ targetPanel: 'shortcuts'
+ },
+ {
+ id: 'shortcuts-view-controls',
+ title: 'View Controls',
+ component: {},
+ type: 'vue',
+ targetPanel: 'shortcuts'
+ }
+ ]
+}))
+
+vi.mock('@/composables/bottomPanelTabs/useTerminalTabs', () => ({
+ useLogsTerminalTab: () => ({
+ id: 'logs',
+ title: 'Logs',
+ component: {},
+ type: 'vue',
+ targetPanel: 'terminal'
+ }),
+ useCommandTerminalTab: () => ({
+ id: 'command',
+ title: 'Command',
+ component: {},
+ type: 'vue',
+ targetPanel: 'terminal'
+ })
+}))
+
+vi.mock('@/stores/commandStore', () => ({
+ useCommandStore: () => ({
+ registerCommand: vi.fn()
+ })
+}))
+
+vi.mock('@/utils/envUtil', () => ({
+ isElectron: () => false
+}))
+
+describe('useBottomPanelStore', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ })
+
+ it('should initialize with empty panels', () => {
+ const store = useBottomPanelStore()
+
+ expect(store.activePanel).toBeNull()
+ expect(store.bottomPanelVisible).toBe(false)
+ expect(store.bottomPanelTabs).toEqual([])
+ expect(store.activeBottomPanelTab).toBeNull()
+ })
+
+ it('should register bottom panel tabs', () => {
+ const store = useBottomPanelStore()
+ const tab: BottomPanelExtension = {
+ id: 'test-tab',
+ title: 'Test Tab',
+ component: {},
+ type: 'vue',
+ targetPanel: 'terminal'
+ }
+
+ store.registerBottomPanelTab(tab)
+
+ expect(store.panels.terminal.tabs.find((t) => t.id === 'test-tab')).toEqual(
+ tab
+ )
+ expect(store.panels.terminal.activeTabId).toBe('test-tab')
+ })
+
+ it('should toggle panel visibility', () => {
+ const store = useBottomPanelStore()
+ const tab: BottomPanelExtension = {
+ id: 'test-tab',
+ title: 'Test Tab',
+ component: {},
+ type: 'vue',
+ targetPanel: 'shortcuts'
+ }
+
+ store.registerBottomPanelTab(tab)
+
+ // Panel should be hidden initially
+ expect(store.activePanel).toBeNull()
+
+ // Toggle should show panel
+ store.togglePanel('shortcuts')
+ expect(store.activePanel).toBe('shortcuts')
+ expect(store.bottomPanelVisible).toBe(true)
+
+ // Toggle again should hide panel
+ store.togglePanel('shortcuts')
+ expect(store.activePanel).toBeNull()
+ expect(store.bottomPanelVisible).toBe(false)
+ })
+
+ it('should switch between panel types', () => {
+ const store = useBottomPanelStore()
+
+ const terminalTab: BottomPanelExtension = {
+ id: 'terminal-tab',
+ title: 'Terminal',
+ component: {},
+ type: 'vue',
+ targetPanel: 'terminal'
+ }
+
+ const shortcutsTab: BottomPanelExtension = {
+ id: 'shortcuts-tab',
+ title: 'Shortcuts',
+ component: {},
+ type: 'vue',
+ targetPanel: 'shortcuts'
+ }
+
+ store.registerBottomPanelTab(terminalTab)
+ store.registerBottomPanelTab(shortcutsTab)
+
+ // Show terminal panel
+ store.togglePanel('terminal')
+ expect(store.activePanel).toBe('terminal')
+ expect(store.activeBottomPanelTab?.id).toBe('terminal-tab')
+
+ // Switch to shortcuts panel
+ store.togglePanel('shortcuts')
+ expect(store.activePanel).toBe('shortcuts')
+ expect(store.activeBottomPanelTab?.id).toBe('shortcuts-tab')
+ })
+
+ it('should toggle specific tabs', () => {
+ const store = useBottomPanelStore()
+ const tab: BottomPanelExtension = {
+ id: 'specific-tab',
+ title: 'Specific Tab',
+ component: {},
+ type: 'vue',
+ targetPanel: 'shortcuts'
+ }
+
+ store.registerBottomPanelTab(tab)
+
+ // Toggle specific tab should show it
+ store.toggleBottomPanelTab('specific-tab')
+ expect(store.activePanel).toBe('shortcuts')
+ expect(store.panels.shortcuts.activeTabId).toBe('specific-tab')
+
+ // Toggle same tab again should hide panel
+ store.toggleBottomPanelTab('specific-tab')
+ expect(store.activePanel).toBeNull()
+ })
+})