Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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<ShortcutRow[]>(() => {
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -105,6 +106,78 @@ describe('collectAllKeybindings', () => {
})
})

describe('filterCommandsByWhen', () => {
function makeContext(overrides: Partial<WhenContext> = {}): 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)
Expand Down
15 changes: 6 additions & 9 deletions packages/core/src/client/webcomponents/state/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)
Expand All @@ -44,6 +45,7 @@ export async function createCommandsContext(
dockOpen: false,
paletteOpen: paletteOpen.value,
dockSelectedId: '',
popupOpen: isDockPopupOpen.value,
}
}

Expand All @@ -55,13 +57,8 @@ export async function createCommandsContext(

const paletteCommands = computed<DevToolsCommandEntry[]>(() => {
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 {
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/client/webcomponents/state/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
},
Expand All @@ -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'
},
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/client/webcomponents/state/keybindings.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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[],
Expand Down
Loading