Skip to content

Commit 5b4802f

Browse files
antfuclaude
andcommitted
refactor: move when evaluator to kit, replace isHidden with when on docks
Move evaluateWhen/WhenContext/getContextValue to packages/kit/src/utils/when.ts as a dedicated module exported from @vitejs/devtools-kit. Core keybindings.ts now re-exports from kit. Replace isHidden?: boolean on DevToolsDockEntryBase with when?: string, using the same expression syntax as command when clauses. The ~terminals dock now uses `get when() { return sessions.size === 0 ? 'false' : undefined }`. evaluateWhen now supports 'true' and 'false' as literal values, enabling unconditional hide/show without a context variable. DockEntries.vue evaluates dock when clauses client-side with a reactive WhenContext. docksGroupByCategories accepts optional whenContext for filtering. Comprehensive when.test.ts in kit (32 test files, 245 tests total). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c9505f4 commit 5b4802f

File tree

12 files changed

+325
-213
lines changed

12 files changed

+325
-213
lines changed

packages/core/src/client/webcomponents/components/dock/DockEntries.vue

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<script setup lang="ts">
2-
import type { DevToolsDockEntry } from '@vitejs/devtools-kit'
2+
import type { DevToolsDockEntry, WhenContext } from '@vitejs/devtools-kit'
33
import type { DocksContext } from '@vitejs/devtools-kit/client'
4-
import { toRefs } from 'vue'
4+
import { evaluateWhen } from '@vitejs/devtools-kit'
5+
import { computed, toRefs } from 'vue'
56
import DockEntry from './DockEntry.vue'
67
78
const props = defineProps<{
@@ -17,6 +18,19 @@ const emit = defineEmits<{
1718
1819
const { selected, isVertical, entries } = toRefs(props)
1920
21+
const whenContext = computed<WhenContext>(() => ({
22+
clientType: props.context.clientType,
23+
dockOpen: props.context.panel.store.open,
24+
paletteOpen: props.context.commands.paletteOpen,
25+
dockSelectedId: props.context.docks.selectedId ?? '',
26+
}))
27+
28+
function isDockVisible(dock: DevToolsDockEntry): boolean {
29+
if (!dock.when)
30+
return true
31+
return evaluateWhen(dock.when, whenContext.value)
32+
}
33+
2034
function toggleDockEntry(dock: DevToolsDockEntry) {
2135
if (selected.value?.id === dock.id)
2236
emit('select', undefined!)
@@ -28,7 +42,7 @@ function toggleDockEntry(dock: DevToolsDockEntry) {
2842
<template>
2943
<template v-for="dock of entries" :key="dock.id">
3044
<DockEntry
31-
v-if="!dock.isHidden"
45+
v-if="isDockVisible(dock)"
3246
:context="context"
3347
:dock
3448
:is-selected="selected?.id === dock.id"

packages/core/src/client/webcomponents/state/__tests__/keybindings.test.ts

Lines changed: 1 addition & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { DevToolsCommandEntry, DevToolsCommandKeybinding } from '@vitejs/devtools-kit'
2-
import type { WhenContext } from '../keybindings'
32
import { describe, expect, it } from 'vitest'
4-
import { collectAllKeybindings, evaluateWhen, formatKeybinding, KNOWN_BROWSER_SHORTCUTS, normalizeKeyEvent } from '../keybindings'
3+
import { collectAllKeybindings, formatKeybinding, KNOWN_BROWSER_SHORTCUTS, normalizeKeyEvent } from '../keybindings'
54

65
describe('formatKeybinding', () => {
76
it('splits key string into parts', () => {
@@ -10,7 +9,6 @@ describe('formatKeybinding', () => {
109

1110
it('formats modifier keys', () => {
1211
const result = formatKeybinding('Mod+Shift+K')
13-
// Platform-dependent, but should have 3 parts
1412
expect(result).toHaveLength(3)
1513
expect(result[2]).toBe('K')
1614
})
@@ -41,7 +39,6 @@ describe('normalizeKeyEvent', () => {
4139

4240
it('ignores modifier-only events', () => {
4341
const result = normalizeKeyEvent(makeKeyEvent({ key: 'Control', ctrlKey: true }))
44-
// On non-Mac, ctrlKey maps to Mod; key is Control which is filtered out
4542
expect(result).toMatch(/^(Mod|Ctrl)?$/)
4643
})
4744

@@ -56,145 +53,6 @@ describe('normalizeKeyEvent', () => {
5653
})
5754
})
5855

59-
describe('evaluateWhen', () => {
60-
const ctx: WhenContext = {
61-
clientType: 'embedded',
62-
dockOpen: true,
63-
paletteOpen: false,
64-
dockSelectedId: 'my-dock',
65-
}
66-
67-
describe('bare truthy', () => {
68-
it('evaluates true for truthy values', () => {
69-
expect(evaluateWhen('dockOpen', ctx)).toBe(true)
70-
})
71-
72-
it('evaluates false for falsy values', () => {
73-
expect(evaluateWhen('paletteOpen', ctx)).toBe(false)
74-
})
75-
76-
it('evaluates true for non-empty string', () => {
77-
expect(evaluateWhen('dockSelectedId', ctx)).toBe(true)
78-
})
79-
80-
it('evaluates false for undefined keys', () => {
81-
expect(evaluateWhen('unknownKey', ctx)).toBe(false)
82-
})
83-
})
84-
85-
describe('negation (!)', () => {
86-
it('negates truthy to false', () => {
87-
expect(evaluateWhen('!dockOpen', ctx)).toBe(false)
88-
})
89-
90-
it('negates falsy to true', () => {
91-
expect(evaluateWhen('!paletteOpen', ctx)).toBe(true)
92-
})
93-
94-
it('negates undefined to true', () => {
95-
expect(evaluateWhen('!unknownKey', ctx)).toBe(true)
96-
})
97-
})
98-
99-
describe('equality (==)', () => {
100-
it('matches string values', () => {
101-
expect(evaluateWhen('clientType == embedded', ctx)).toBe(true)
102-
})
103-
104-
it('rejects non-matching string values', () => {
105-
expect(evaluateWhen('clientType == standalone', ctx)).toBe(false)
106-
})
107-
108-
it('compares boolean as string', () => {
109-
expect(evaluateWhen('dockOpen == true', ctx)).toBe(true)
110-
expect(evaluateWhen('dockOpen == false', ctx)).toBe(false)
111-
})
112-
113-
it('matches dockSelectedId', () => {
114-
expect(evaluateWhen('dockSelectedId == my-dock', ctx)).toBe(true)
115-
expect(evaluateWhen('dockSelectedId == other-dock', ctx)).toBe(false)
116-
})
117-
})
118-
119-
describe('inequality (!=)', () => {
120-
it('true when values differ', () => {
121-
expect(evaluateWhen('clientType != standalone', ctx)).toBe(true)
122-
})
123-
124-
it('false when values match', () => {
125-
expect(evaluateWhen('clientType != embedded', ctx)).toBe(false)
126-
})
127-
})
128-
129-
describe('aND (&&)', () => {
130-
it('true when all parts are true', () => {
131-
expect(evaluateWhen('dockOpen && !paletteOpen', ctx)).toBe(true)
132-
})
133-
134-
it('false when any part is false', () => {
135-
expect(evaluateWhen('dockOpen && paletteOpen', ctx)).toBe(false)
136-
})
137-
138-
it('supports three-part AND', () => {
139-
expect(evaluateWhen('dockOpen && !paletteOpen && clientType == embedded', ctx)).toBe(true)
140-
expect(evaluateWhen('dockOpen && !paletteOpen && clientType == standalone', ctx)).toBe(false)
141-
})
142-
})
143-
144-
describe('oR (||)', () => {
145-
it('true when any part is true', () => {
146-
expect(evaluateWhen('paletteOpen || dockOpen', ctx)).toBe(true)
147-
})
148-
149-
it('false when all parts are false', () => {
150-
expect(evaluateWhen('paletteOpen || !dockOpen', ctx)).toBe(false)
151-
})
152-
153-
it('supports mixed AND and OR (OR of ANDs)', () => {
154-
// paletteOpen=false, so first branch fails; dockOpen=true, so second branch succeeds
155-
expect(evaluateWhen('paletteOpen && clientType == standalone || dockOpen', ctx)).toBe(true)
156-
})
157-
})
158-
159-
describe('with empty dockSelectedId', () => {
160-
const emptyCtx: WhenContext = {
161-
clientType: 'standalone',
162-
dockOpen: false,
163-
paletteOpen: true,
164-
dockSelectedId: '',
165-
}
166-
167-
it('empty string is falsy', () => {
168-
expect(evaluateWhen('dockSelectedId', emptyCtx)).toBe(false)
169-
})
170-
171-
it('negation of empty string is true', () => {
172-
expect(evaluateWhen('!dockSelectedId', emptyCtx)).toBe(true)
173-
})
174-
})
175-
176-
describe('with custom context variables', () => {
177-
const customCtx: WhenContext = {
178-
clientType: 'embedded',
179-
dockOpen: true,
180-
paletteOpen: false,
181-
dockSelectedId: '',
182-
myPluginActive: true,
183-
myPluginMode: 'debug',
184-
}
185-
186-
it('evaluates custom boolean', () => {
187-
expect(evaluateWhen('myPluginActive', customCtx)).toBe(true)
188-
expect(evaluateWhen('!myPluginActive', customCtx)).toBe(false)
189-
})
190-
191-
it('evaluates custom string equality', () => {
192-
expect(evaluateWhen('myPluginMode == debug', customCtx)).toBe(true)
193-
expect(evaluateWhen('myPluginMode == release', customCtx)).toBe(false)
194-
})
195-
})
196-
})
197-
19856
describe('collectAllKeybindings', () => {
19957
it('collects from top-level and children', () => {
20058
const commands = {

packages/core/src/client/webcomponents/state/context.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DevToolsClientCommand } from '@vitejs/devtools-kit'
1+
import type { DevToolsClientCommand, WhenContext } from '@vitejs/devtools-kit'
22
import type { CommandsContext, DevToolsRpcClient, DockClientScriptContext, DockEntryState, DockPanelStorage, DocksContext } from '@vitejs/devtools-kit/client'
33
import type { SharedState } from '@vitejs/devtools-kit/utils/shared-state'
44
import type { Ref } from 'vue'
@@ -109,18 +109,22 @@ export async function createDocksContext(
109109
// Get settings store and create computed grouped entries
110110
const settingsStore = markRaw(await getSettingsStore())
111111
const settings = sharedStateToRef(settingsStore)
112-
const groupedEntries = computed(() => {
113-
return docksGroupByCategories(dockEntries.value, settings.value)
114-
})
115112

116-
// Initialize commands context with reactive when-context
113+
// Shared when-context provider — used by both commands and docks
117114
let commandsContext: CommandsContext
118-
const commandsContextResult = await createCommandsContext(clientType, rpc, () => ({
115+
const getWhenContext = (): WhenContext => ({
119116
clientType,
120117
dockOpen: panelStore.value.open,
121118
paletteOpen: commandsContext?.paletteOpen ?? false,
122119
dockSelectedId: selectedId.value ?? '',
123-
}))
120+
})
121+
122+
const groupedEntries = computed(() => {
123+
return docksGroupByCategories(dockEntries.value, settings.value, { whenContext: getWhenContext() })
124+
})
125+
126+
// Initialize commands context with reactive when-context
127+
const commandsContextResult = await createCommandsContext(clientType, rpc, getWhenContext)
124128
commandsContext = commandsContextResult
125129

126130
// Register built-in client commands
@@ -194,11 +198,10 @@ export async function createDocksContext(
194198
const dockChildren: DevToolsClientCommand[] = dockEntries.value
195199
.filter(entry => entry.type !== '~builtin')
196200
.map((entry) => {
197-
const isAction = entry.type === 'action'
198201
return {
199202
id: `devtools:docks:${entry.id}`,
200203
source: 'client' as const,
201-
title: `${isAction ? 'Execute' : 'Open'} ${entry.title}`,
204+
title: entry.title,
202205
icon: typeof entry.icon === 'string' ? entry.icon : undefined,
203206
action: () => {
204207
switchEntry(entry.id)

packages/core/src/client/webcomponents/state/dock-settings.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import type { DevToolsDockEntriesGrouped, DevToolsDockEntry, DevToolsDocksUserSettings } from '@vitejs/devtools-kit'
1+
import type { DevToolsDockEntriesGrouped, DevToolsDockEntry, DevToolsDocksUserSettings, WhenContext } from '@vitejs/devtools-kit'
22
import type { Immutable } from '@vitejs/devtools-kit/utils/shared-state'
3+
import { evaluateWhen } from '@vitejs/devtools-kit'
34
import { DEFAULT_CATEGORIES_ORDER } from '../constants'
45

56
export type { DevToolsDocksUserSettings }
@@ -17,15 +18,17 @@ export interface SplitGroupsResult {
1718
export function docksGroupByCategories(
1819
entries: DevToolsDockEntry[],
1920
settings: Immutable<DevToolsDocksUserSettings>,
20-
options?: { includeHidden?: boolean },
21+
options?: { includeHidden?: boolean, whenContext?: WhenContext },
2122
): DevToolsDockEntriesGrouped {
2223
const { docksHidden, docksCategoriesHidden, docksCustomOrder, docksPinned } = settings
23-
const { includeHidden = false } = options ?? {}
24+
const { includeHidden = false, whenContext } = options ?? {}
2425

2526
const map = new Map<string, DevToolsDockEntry[]>()
2627
for (const entry of entries) {
27-
// Skip if hidden by entry property
28-
if (entry.isHidden && !includeHidden)
28+
// Skip if hidden by `when` clause
29+
if (entry.when && whenContext && !evaluateWhen(entry.when, whenContext) && !includeHidden)
30+
continue
31+
if (entry.when && !whenContext && entry.when === 'false' && !includeHidden)
2932
continue
3033
if (!includeHidden && docksHidden.includes(entry.id))
3134
continue

packages/core/src/client/webcomponents/state/keybindings.ts

Lines changed: 3 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,8 @@
11
import type { DevToolsCommandEntry, DevToolsCommandKeybinding } from '@vitejs/devtools-kit'
22

3-
export interface WhenContext {
4-
clientType: 'embedded' | 'standalone'
5-
dockOpen: boolean
6-
paletteOpen: boolean
7-
dockSelectedId: string
8-
/** Allow custom context variables from plugins */
9-
[key: string]: unknown
10-
}
11-
12-
export function evaluateWhen(expression: string, ctx: WhenContext): boolean {
13-
// Simple expression evaluator: supports ==, !=, &&, ||, !, bare truthy
14-
const parts = expression.split('||').map(s => s.trim())
15-
return parts.some((orPart) => {
16-
const andParts = orPart.split('&&').map(s => s.trim())
17-
return andParts.every((part) => {
18-
const trimmed = part.trim()
19-
20-
// Negation
21-
if (trimmed.startsWith('!')) {
22-
const key = trimmed.slice(1).trim()
23-
return !getContextValue(key, ctx)
24-
}
25-
26-
// Equality/inequality
27-
const eqIdx = trimmed.indexOf('==')
28-
const neqIdx = trimmed.indexOf('!=')
29-
if (eqIdx !== -1 || neqIdx !== -1) {
30-
const isNeq = neqIdx !== -1 && (eqIdx === -1 || neqIdx < eqIdx)
31-
const opIdx = isNeq ? neqIdx : eqIdx
32-
const opLen = isNeq ? 2 : 2
33-
const key = trimmed.slice(0, opIdx).trim()
34-
const value = trimmed.slice(opIdx + opLen).trim()
35-
const actual = String(getContextValue(key, ctx))
36-
return isNeq ? actual !== value : actual === value
37-
}
38-
39-
// Bare truthy
40-
return !!getContextValue(trimmed, ctx)
41-
})
42-
})
43-
}
44-
45-
export function getContextValue(key: string, ctx: WhenContext): unknown {
46-
return (ctx as unknown as Record<string, unknown>)[key]
47-
}
3+
// Re-export when utilities from kit
4+
export type { WhenContext } from '@vitejs/devtools-kit'
5+
export { evaluateWhen, getContextValue } from '@vitejs/devtools-kit'
486

497
export const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform ?? '')
508

packages/core/src/node/host-docks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export class DevToolsDockHost implements DevToolsDockHostType {
3838
title: 'Terminals',
3939
icon: 'ph:terminal-duotone',
4040
category: '~builtin',
41-
get isHidden() {
42-
return context.terminals.sessions.size === 0
41+
get when() {
42+
return context.terminals.sessions.size === 0 ? 'false' : undefined
4343
},
4444
},
4545
{

packages/kit/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export type * from './types'
22
export * from './utils/define'
33
export * from './utils/json-render'
4+
export * from './utils/when'

packages/kit/src/types/docks.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,16 @@ export interface DevToolsDockEntryBase {
3434
*/
3535
category?: DevToolsDockEntryCategory
3636
/**
37-
* Whether the entry should be hidden from the user.
38-
* @default false
37+
* Conditional visibility expression.
38+
* When set, the dock entry is only visible when the expression evaluates to true.
39+
* Uses the same syntax as command `when` clauses.
40+
*
41+
* Set to `'false'` to unconditionally hide the entry.
42+
*
43+
* @example 'clientType == embedded'
44+
* @see {@link import('../utils/when').evaluateWhen}
3945
*/
40-
isHidden?: boolean
46+
when?: string
4147
/**
4248
* Badge text to display on the dock icon (e.g., unread count)
4349
*/

0 commit comments

Comments
 (0)