From c17c8071747a8b2fd403429cf2caba6b7eec155a Mon Sep 17 00:00:00 2001 From: yu859 <15715093608@163.com> Date: Sat, 1 Aug 2026 12:42:47 +0800 Subject: [PATCH] fix(core): hide dock mode commands in popup mode --- .../views-builtin/SettingsShortcuts.vue | 9 ++- .../state/__tests__/keybindings.test.ts | 75 ++++++++++++++++++- .../client/webcomponents/state/commands.ts | 15 ++-- .../src/client/webcomponents/state/context.ts | 13 +++- .../client/webcomponents/state/keybindings.ts | 34 +++++++++ 5 files changed, 132 insertions(+), 14 deletions(-) diff --git a/packages/core/src/client/webcomponents/components/views-builtin/SettingsShortcuts.vue b/packages/core/src/client/webcomponents/components/views-builtin/SettingsShortcuts.vue index 08f6f70ae..60760e919 100644 --- a/packages/core/src/client/webcomponents/components/views-builtin/SettingsShortcuts.vue +++ b/packages/core/src/client/webcomponents/components/views-builtin/SettingsShortcuts.vue @@ -3,7 +3,7 @@ import type { DevToolsCommandEntry, DevToolsCommandKeybinding } from '@vitejs/de import type { DocksContext } from '@vitejs/devtools-kit/client' import { computed, nextTick, ref, watch } from 'vue' import { sharedStateToRef } from '../../state/docks' -import { formatKeybinding, isKeybindingOverrideDifferentFromDefault, isMac, KNOWN_BROWSER_SHORTCUTS } from '../../state/keybindings' +import { filterCommandsByWhen, formatKeybinding, isKeybindingOverrideDifferentFromDefault, isMac, KNOWN_BROWSER_SHORTCUTS } from '../../state/keybindings' import KeybindingBadge from '../command-palette/KeybindingBadge.vue' import DockIcon from '../dock/DockIcon.vue' @@ -22,9 +22,14 @@ interface ShortcutRow { indent: boolean } +// Only offer to bind commands that are actually reachable right now — binding a +// key to something the current context rules out (e.g. the dock-mode commands +// while the dock is detached into a popup) would silently do nothing. +const availableCommands = computed(() => filterCommandsByWhen(commandsCtx.commands, props.context.when.context)) + const shortcutRows = computed(() => { const rows: ShortcutRow[] = [] - for (const cmd of commandsCtx.commands) { + for (const cmd of availableCommands.value) { rows.push({ command: cmd, indent: false }) if (cmd.children) { for (const child of cmd.children) { diff --git a/packages/core/src/client/webcomponents/state/__tests__/keybindings.test.ts b/packages/core/src/client/webcomponents/state/__tests__/keybindings.test.ts index df037914a..741f77066 100644 --- a/packages/core/src/client/webcomponents/state/__tests__/keybindings.test.ts +++ b/packages/core/src/client/webcomponents/state/__tests__/keybindings.test.ts @@ -1,6 +1,7 @@ import type { DevToolsCommandEntry, DevToolsCommandKeybinding } from '@vitejs/devtools-kit' +import type { WhenContext } from '../keybindings' import { describe, expect, it } from 'vitest' -import { areKeybindingsEqual, collectAllKeybindings, formatKeybinding, isKeybindingOverrideDifferentFromDefault, KNOWN_BROWSER_SHORTCUTS, normalizeKeyEvent } from '../keybindings' +import { areKeybindingsEqual, collectAllKeybindings, filterCommandsByWhen, formatKeybinding, isKeybindingOverrideDifferentFromDefault, KNOWN_BROWSER_SHORTCUTS, normalizeKeyEvent } from '../keybindings' describe('formatKeybinding', () => { it('splits key string into parts', () => { @@ -105,6 +106,78 @@ describe('collectAllKeybindings', () => { }) }) +describe('filterCommandsByWhen', () => { + function makeContext(overrides: Partial = {}): WhenContext { + return { + clientType: 'embedded', + dockOpen: false, + paletteOpen: false, + dockSelectedId: '', + popupOpen: false, + ...overrides, + } + } + + function makeDockMode(): DevToolsCommandEntry[] { + return [ + { + id: 'devtools:dock-mode', + source: 'client' as const, + title: 'Dock Mode', + when: 'clientType == embedded && !popupOpen', + children: [ + { id: 'devtools:dock-mode:float', source: 'client' as const, title: 'Float Mode', when: '!popupOpen' }, + { id: 'devtools:dock-mode:edge', source: 'client' as const, title: 'Edge Mode', when: '!popupOpen' }, + ], + }, + ] as DevToolsCommandEntry[] + } + + it('passes through commands without a when clause', () => { + const commands = [ + { id: 'cmd1', source: 'client' as const, title: 'Cmd 1' }, + { id: 'cmd2', source: 'client' as const, title: 'Cmd 2' }, + ] as DevToolsCommandEntry[] + + expect(filterCommandsByWhen(commands, makeContext())).toEqual(commands) + }) + + it('drops a parent whose when clause fails, children included', () => { + const result = filterCommandsByWhen(makeDockMode(), makeContext({ popupOpen: true })) + expect(result).toHaveLength(0) + }) + + it('keeps a passing parent but removes children whose own when clause fails', () => { + const commands = [ + { + id: 'parent', + source: 'client' as const, + title: 'Parent', + children: [ + { id: 'parent:always', source: 'client' as const, title: 'Always' }, + { id: 'parent:embedded', source: 'client' as const, title: 'Embedded only', when: 'clientType == embedded' }, + ], + }, + ] as DevToolsCommandEntry[] + + const result = filterCommandsByWhen(commands, makeContext({ clientType: 'standalone' })) + expect(result).toHaveLength(1) + expect(result[0]!.children?.map(c => c.id)).toEqual(['parent:always']) + }) + + it('keeps everything when the context satisfies every clause', () => { + const result = filterCommandsByWhen(makeDockMode(), makeContext()) + expect(result).toHaveLength(1) + expect(result[0]!.children).toHaveLength(2) + }) + + it('does not mutate the input commands', () => { + const commands = makeDockMode() + filterCommandsByWhen(commands, makeContext({ popupOpen: true })) + expect(commands[0]!.children).toHaveLength(2) + }) +}) + describe('areKeybindingsEqual', () => { it('treats undefined and empty arrays as equal', () => { expect(areKeybindingsEqual(undefined, [])).toBe(true) diff --git a/packages/core/src/client/webcomponents/state/commands.ts b/packages/core/src/client/webcomponents/state/commands.ts index 20c7444d0..1ce57c6de 100644 --- a/packages/core/src/client/webcomponents/state/commands.ts +++ b/packages/core/src/client/webcomponents/state/commands.ts @@ -6,8 +6,8 @@ import type { ShallowRef } from 'vue' import { evaluateWhen } from 'devframe/utils/when' import { computed, markRaw, reactive, ref, watch } from 'vue' import { sharedStateToRef } from './docks' -import { collectAllKeybindings, normalizeKeyEvent } from './keybindings' -import { useDockPopupWindow } from './popup' +import { collectAllKeybindings, filterCommandsByWhen, normalizeKeyEvent } from './keybindings' +import { useDockPopupWindow, useIsDockPopupOpen } from './popup' export { formatKeybinding, isMac, normalizeKeyEvent } from './keybindings' @@ -35,6 +35,7 @@ export async function createCommandsContext( const shortcutOverrides = computed(() => settings.value.commandShortcuts ?? {}) const paletteOpen = ref(false) + const isDockPopupOpen = useIsDockPopupOpen() const getWhenContext = (): WhenContext => { if (whenContextProvider) @@ -44,6 +45,7 @@ export async function createCommandsContext( dockOpen: false, paletteOpen: paletteOpen.value, dockSelectedId: '', + popupOpen: isDockPopupOpen.value, } } @@ -55,13 +57,8 @@ export async function createCommandsContext( const paletteCommands = computed(() => { const ctx = getWhenContext() - return commands.value.filter((cmd) => { - if (cmd.showInPalette === false) - return false - if (cmd.when && !evaluateWhen(cmd.when, ctx)) - return false - return true - }) + const available = filterCommandsByWhen(commands.value, ctx) + return available.filter(cmd => cmd.showInPalette !== false) }) function register(cmd: DevToolsClientCommand | DevToolsClientCommand[]): () => void { diff --git a/packages/core/src/client/webcomponents/state/context.ts b/packages/core/src/client/webcomponents/state/context.ts index 104aec1cc..8f5255c74 100644 --- a/packages/core/src/client/webcomponents/state/context.ts +++ b/packages/core/src/client/webcomponents/state/context.ts @@ -13,7 +13,7 @@ import { createCommandsContext } from './commands' import { docksGroupByCategories, getCategoryLabel, getGroupMembers, getGroupMembersGrouped, getRegisteredGroupIds, resolveCommandIcon, resolveGroupDefaultChild } from './dock-settings' import { createDockEntryState, DEFAULT_DOCK_PANEL_STORE, sharedStateToRef, useDocksEntries } from './docks' import { createClientMessagesClient } from './messages-client' -import { registerMainFrameDockActionHandler, triggerMainFrameDockAction } from './popup' +import { registerMainFrameDockActionHandler, triggerMainFrameDockAction, useIsDockPopupOpen } from './popup' import { createDockRenderers } from './renderers' import { executeSetupScript } from './setup-script' @@ -131,11 +131,13 @@ export async function createDocksContext( // Shared when-context provider — used by both commands and docks let commandsContext: CommandsContext + const isDockPopupOpen = useIsDockPopupOpen() const getWhenContext = (): WhenContext => ({ clientType, dockOpen: panelStore.value.open, paletteOpen: commandsContext?.paletteOpen ?? false, dockSelectedId: selectedId.value ?? '', + popupOpen: isDockPopupOpen.value, }) // Tracks the shared frame's current member tab, keyed by `frameId`. A @@ -387,13 +389,19 @@ export async function createDocksContext( source: 'client', title: 'Dock Mode', icon: 'ph:layout-duotone', - when: clientType === 'embedded' ? 'clientType == embedded' : undefined, + // While the popup is open the embedded shell is unmounted and the popup + // renders the standalone layout, so neither mode is observable — mirrors + // the Appearance settings hiding its own dock-mode control. + when: clientType === 'embedded' ? 'clientType == embedded && !popupOpen' : undefined, children: [ { id: 'devtools:dock-mode:float', source: 'client', title: 'Float Mode', icon: 'ph:cards-three-duotone', + // Repeated per child: shortcut dispatch reads the matched command's + // own `when` and does not inherit the parent's. + when: '!popupOpen', action: () => { panelStore.value.mode = 'float' }, @@ -403,6 +411,7 @@ export async function createDocksContext( source: 'client', title: 'Edge Mode', icon: 'ph:square-half-bottom-duotone', + when: '!popupOpen', action: () => { panelStore.value.mode = 'edge' }, diff --git a/packages/core/src/client/webcomponents/state/keybindings.ts b/packages/core/src/client/webcomponents/state/keybindings.ts index c0fa1156e..560696f4d 100644 --- a/packages/core/src/client/webcomponents/state/keybindings.ts +++ b/packages/core/src/client/webcomponents/state/keybindings.ts @@ -1,4 +1,6 @@ import type { DevToolsCommandEntry, DevToolsCommandKeybinding } from '@vitejs/devtools-kit' +import type { WhenContext } from 'devframe/utils/when' +import { evaluateWhen } from 'devframe/utils/when' export type { WhenContext } from 'devframe/utils/when' export { evaluateWhen, resolveContextValue } from 'devframe/utils/when' @@ -57,6 +59,38 @@ export function isKeybindingOverrideDifferentFromDefault( return override !== undefined && !areKeybindingsEqual(override, defaults) } +/** + * Drop the commands whose `when` clause does not hold in the current context, + * children included — `when` is documented to control palette visibility, but + * nothing evaluated it for nested entries. + * + * A parent that survives is shallow-cloned so its `children` can be narrowed + * without mutating the registry. Callers therefore get fresh parent objects on + * every call: match entries by `id`, never by reference. + */ +export function filterCommandsByWhen( + commands: DevToolsCommandEntry[], + ctx: WhenContext, +): DevToolsCommandEntry[] { + const isAvailable = (cmd: { when?: string }) => !cmd.when || evaluateWhen(cmd.when, ctx) + + const result: DevToolsCommandEntry[] = [] + for (const cmd of commands) { + if (!isAvailable(cmd)) + continue + if (!cmd.children) { + result.push(cmd) + continue + } + // `children` is typed `Server[] | Client[]` rather than `(Server | Client)[]`, + // so filtering it in place widens the element type — same cast the other + // child-walking call sites use. + const children = (cmd.children as DevToolsCommandEntry[]).filter(isAvailable) + result.push({ ...cmd, children } as DevToolsCommandEntry) + } + return result +} + export function collectAllKeybindings( commands: { value: DevToolsCommandEntry[] }, getKeybindings: (id: string) => DevToolsCommandKeybinding[],