Skip to content

Commit 67cf430

Browse files
authored
feat: hide Devframe Inspector by default and auto-hide empty terminals/messages docks (#399)
1 parent b807a80 commit 67cf430

11 files changed

Lines changed: 202 additions & 12 deletions

File tree

‎packages/core/playground/vite.config.ts‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { A11yCheckerPlugin } from '../../../examples/plugin-a11y-checker/src/nod
1313
import { GitUIPlugin } from '../../../examples/plugin-git-ui/src/node'
1414
import { DevTools } from '../../core/src'
1515
import { buildCSS } from '../../core/src/client/webcomponents/scripts/build-css'
16+
import { hideDockWhenEmpty } from '../../core/src/node/plugins/auto-hide'
1617
// eslint-disable-next-line ts/ban-ts-comment
1718
// @ts-ignore ignore the type error
1819
import { DevToolsRolldownUI } from '../../rolldown/src/node'
@@ -35,12 +36,27 @@ export default defineConfig({
3536
plugins: [
3637
VueRouter(),
3738
Vue(),
38-
createPluginFromDevframe(createTerminalsDevframe(), {
39-
dock: { category: '~builtin' },
40-
}),
41-
createPluginFromDevframe(createMessagesDevframe(), {
42-
dock: { category: '~builtin' },
43-
}),
39+
...(() => {
40+
// Mirror the shipped `DevTools()` mounts (the playground runs with
41+
// `builtinDevTools: false`, so it re-creates them by hand): terminals
42+
// and messages auto-hide from the dock bar while empty.
43+
const terminalsDevframe = createTerminalsDevframe()
44+
const messagesDevframe = createMessagesDevframe()
45+
return [
46+
createPluginFromDevframe(terminalsDevframe, {
47+
dock: { category: '~builtin' },
48+
setup(ctx) {
49+
hideDockWhenEmpty(ctx, terminalsDevframe.id, () => ctx.terminals.sessions.size === 0)
50+
},
51+
}),
52+
createPluginFromDevframe(messagesDevframe, {
53+
dock: { category: '~builtin' },
54+
setup(ctx) {
55+
hideDockWhenEmpty(ctx, messagesDevframe.id, () => ctx.messages.entries.size === 0)
56+
},
57+
}),
58+
]
59+
})(),
4460
createPluginFromDevframe(createInspectDevframe(), {
4561
dock: { category: '~builtin', icon: 'ph:stethoscope-duotone' },
4662
}),

‎packages/core/src/client/webcomponents/components/views-builtin/SettingsAdvanced.vue‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import type { SharedState } from 'devframe/utils/shared-state'
44
import type { DevToolsDocksUserSettings } from '../../state/dock-settings'
55
import { DEFAULT_STATE_USER_SETTINGS } from '@vitejs/devtools-kit/constants'
66
import { useConfirm } from '../../state/confirm'
7+
import { sharedStateToRef } from '../../state/docks'
78
89
const props = defineProps<{
910
context: DocksContext
1011
settingsStore: SharedState<DevToolsDocksUserSettings>
1112
}>()
1213
14+
const settings = sharedStateToRef(props.settingsStore)
1315
const confirm = useConfirm()
1416
1517
async function resetAllSettings() {
@@ -64,6 +66,26 @@ async function deauthorize() {
6466

6567
<template>
6668
<div class="flex flex-col gap-6">
69+
<!-- Show Devframe Inspector toggle -->
70+
<label class="flex items-center gap-3 cursor-pointer group">
71+
<button
72+
class="w-10 h-6 rounded-full transition-colors relative shrink-0"
73+
:class="settings.showDevframeInspector ? 'bg-lime' : 'bg-gray/30'"
74+
@click="settingsStore.mutate((s) => { s.showDevframeInspector = !s.showDevframeInspector })"
75+
>
76+
<div
77+
class="absolute top-1 w-4 h-4 rounded-full bg-white shadow transition-transform"
78+
:class="settings.showDevframeInspector ? 'translate-x-5' : 'translate-x-1'"
79+
/>
80+
</button>
81+
<div class="flex flex-col">
82+
<span class="text-sm">Show Devframe Inspector</span>
83+
<span class="text-xs op50">Reveal the experimental Devframe Inspector dock — the DevTools for the DevTools</span>
84+
</div>
85+
</label>
86+
87+
<div class="border-t border-base" />
88+
6789
<!-- Reset Shortcuts -->
6890
<div class="flex items-start gap-4">
6991
<div class="flex-1">

‎packages/core/src/client/webcomponents/state/__tests__/dock-groups.test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DevToolsDockEntry } from '@vitejs/devtools-kit'
2-
import { DEFAULT_STATE_USER_SETTINGS } from '@vitejs/devtools-kit/constants'
2+
import { DEFAULT_STATE_USER_SETTINGS, DEVTOOLS_INSPECTOR_DOCK_ID } from '@vitejs/devtools-kit/constants'
33
import { describe, expect, it } from 'vitest'
44
import {
55
docksGroupByCategories,
@@ -92,6 +92,33 @@ describe('dock groups', () => {
9292
})
9393
})
9494

95+
describe('devframe inspector visibility (settings: showDevframeInspector)', () => {
96+
const entries: DevToolsDockEntry[] = [
97+
iframe('a'),
98+
iframe(DEVTOOLS_INSPECTOR_DOCK_ID),
99+
]
100+
101+
it('hides the inspector from the dock bar by default', () => {
102+
const grouped = docksGroupByCategories(entries, settings)
103+
const ids = grouped.flatMap(([, items]) => items.map(i => i.id))
104+
expect(ids).toContain('a')
105+
expect(ids).not.toContain(DEVTOOLS_INSPECTOR_DOCK_ID)
106+
})
107+
108+
it('reveals the inspector when showDevframeInspector is enabled', () => {
109+
const enabled = { ...settings, showDevframeInspector: true }
110+
const grouped = docksGroupByCategories(entries, enabled)
111+
const ids = grouped.flatMap(([, items]) => items.map(i => i.id))
112+
expect(ids).toContain(DEVTOOLS_INSPECTOR_DOCK_ID)
113+
})
114+
115+
it('still lists the inspector in the settings management view (includeHidden)', () => {
116+
const grouped = docksGroupByCategories(entries, settings, { includeHidden: true })
117+
const ids = grouped.flatMap(([, items]) => items.map(i => i.id))
118+
expect(ids).toContain(DEVTOOLS_INSPECTOR_DOCK_ID)
119+
})
120+
})
121+
95122
describe('resolveCommandIcon (dock icon → command icon projection)', () => {
96123
it('passes string icons through unchanged', () => {
97124
expect(resolveCommandIcon('logos:nuxt-icon')).toBe('logos:nuxt-icon')

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { DevToolsDockEntriesGrouped, DevToolsDockEntry, DevToolsDocksUserSettings, DevToolsViewGroup } from '@vitejs/devtools-kit'
22
import type { Immutable } from 'devframe/utils/shared-state'
33
import type { WhenContext } from 'devframe/utils/when'
4+
import { DEVTOOLS_INSPECTOR_DOCK_ID } from '@vitejs/devtools-kit/constants'
45
import { evaluateWhen } from 'devframe/utils/when'
56
import { DEFAULT_CATEGORIES_ORDER } from '../constants'
67

@@ -104,6 +105,11 @@ export function docksGroupByCategories(
104105
continue
105106
if (entry.when && !whenContext && entry.when === 'false' && !includeHidden)
106107
continue
108+
// The Devframe Inspector is hidden by default; it only joins the dock bar
109+
// once opted into via Settings → Advanced. The settings management view
110+
// (`includeHidden`) still lists it so users can discover the toggle.
111+
if (!includeHidden && entry.id === DEVTOOLS_INSPECTOR_DOCK_ID && !settings.showDevframeInspector)
112+
continue
107113
if (!includeHidden && docksHidden.includes(entry.id))
108114
continue
109115

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { KitNodeContext } from '@vitejs/devtools-kit/node'
2+
import { describe, expect, it } from 'vitest'
3+
import { hideDockWhenEmpty } from '../auto-hide'
4+
5+
interface FakeView { id: string, type: string, url: string, title: string, when?: string }
6+
7+
function fakeCtx(views: FakeView[]): { ctx: KitNodeContext, get: (id: string) => FakeView } {
8+
const map = new Map(views.map(v => [v.id, v]))
9+
const ctx = { docks: { views: map } } as unknown as KitNodeContext
10+
return { ctx, get: id => map.get(id)! }
11+
}
12+
13+
describe('hideDockWhenEmpty', () => {
14+
it('resolves `when` live from the isEmpty callback', () => {
15+
let empty = true
16+
const { ctx, get } = fakeCtx([{ id: 'x', type: 'iframe', url: '/', title: 'X' }])
17+
18+
hideDockWhenEmpty(ctx, 'x', () => empty)
19+
20+
// Hidden while empty, visible once populated — re-read on every access.
21+
expect(get('x').when).toBe('false')
22+
empty = false
23+
expect(get('x').when).toBeUndefined()
24+
empty = true
25+
expect(get('x').when).toBe('false')
26+
})
27+
28+
it('attaches an enumerable `when` so it survives serialization', () => {
29+
const { ctx, get } = fakeCtx([{ id: 'x', type: 'iframe', url: '/', title: 'X' }])
30+
hideDockWhenEmpty(ctx, 'x', () => true)
31+
32+
// Enumerable getters are picked up by JSON.stringify (the shared-state wire
33+
// format), so the resolved value reaches the client filter.
34+
expect(JSON.parse(JSON.stringify(get('x'))).when).toBe('false')
35+
})
36+
37+
it('is a no-op when the dock id is not registered', () => {
38+
const { ctx } = fakeCtx([])
39+
expect(() => hideDockWhenEmpty(ctx, 'missing', () => true)).not.toThrow()
40+
})
41+
})
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { KitNodeContext } from '@vitejs/devtools-kit/node'
2+
3+
/**
4+
* Keep a dock entry out of the dock bar while its backing collection is empty.
5+
*
6+
* Mirrors the hub's built-in `~terminals` / `~messages` docks: attaches a live
7+
* `when` getter to the registered entry that resolves to `'false'`
8+
* (unconditionally hidden) while `isEmpty()` and `undefined` (visible)
9+
* otherwise. The hub already re-serializes the dock shared state on every
10+
* terminal / message change, so the getter is re-read at exactly the right
11+
* moments — no explicit event subscription needed here.
12+
*
13+
* Call this from the `setup(ctx)` hook of the `createPluginFromDevframe` mount,
14+
* after the auto-derived dock entry has been registered.
15+
*
16+
* TODO: once devframe/hub ships first-class support for a functional `when`
17+
* (`when?: () => string | boolean | undefined`, resolved during dock
18+
* serialization), this can become a plain `dock: { when: () => ... }` option
19+
* on `createPluginFromDevframe` and the getter trick can be deleted.
20+
*/
21+
export function hideDockWhenEmpty(
22+
ctx: KitNodeContext,
23+
dockId: string,
24+
isEmpty: () => boolean,
25+
): void {
26+
const view = ctx.docks.views.get(dockId)
27+
if (!view)
28+
return
29+
Object.defineProperty(view, 'when', {
30+
enumerable: true,
31+
configurable: true,
32+
get: () => (isEmpty() ? 'false' : undefined),
33+
})
34+
}

‎packages/core/src/node/plugins/index.ts‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import type { Plugin } from 'vite'
22
import { createInspectDevframe } from '@devframes/plugin-inspect'
33
import { createMessagesDevframe } from '@devframes/plugin-messages'
44
import { createTerminalsDevframe } from '@devframes/plugin-terminals'
5+
import { DEVTOOLS_INSPECTOR_DOCK_ID } from '@vitejs/devtools-kit/constants'
56
import { createPluginFromDevframe } from '@vitejs/devtools-kit/node'
7+
import { hideDockWhenEmpty } from './auto-hide'
68
import { DevToolsBuild } from './build'
79
import { DevToolsInjection } from './injection'
810
import { DevToolsServer } from './server'
@@ -58,17 +60,32 @@ export async function DevTools(options: DevToolsOptions = {}): Promise<Plugin[]>
5860
// dock — rather than the `~viteplus` group (which collects integrations
5961
// like Rolldown). The hub's own `~terminals` / `~messages` docks are
6062
// suppressed via `builtinDocks` in `createDevToolsContext`.
61-
plugins.push(createPluginFromDevframe(createTerminalsDevframe(), {
63+
//
64+
// The hub's built-in `~terminals` / `~messages` docks auto-hid themselves
65+
// when empty; the plugin-mounted iframe docks that replaced them carry no
66+
// such rule, so we restore it here — the dock is filtered out of the bar
67+
// (`when: 'false'`) whenever there are no sessions / messages.
68+
const terminalsDevframe = createTerminalsDevframe()
69+
plugins.push(createPluginFromDevframe(terminalsDevframe, {
6270
dock: { category: '~builtin' },
71+
setup(ctx) {
72+
hideDockWhenEmpty(ctx, terminalsDevframe.id, () => ctx.terminals.sessions.size === 0)
73+
},
6374
}))
64-
plugins.push(createPluginFromDevframe(createMessagesDevframe(), {
75+
const messagesDevframe = createMessagesDevframe()
76+
plugins.push(createPluginFromDevframe(messagesDevframe, {
6577
dock: { category: '~builtin' },
78+
setup(ctx) {
79+
hideDockWhenEmpty(ctx, messagesDevframe.id, () => ctx.messages.entries.size === 0)
80+
},
6681
}))
6782

6883
// Meta-introspection ("DevTools for the DevTools"), provided by the
6984
// official devframe inspector plugin (replaces the former
70-
// `@vitejs/devtools-self-inspect` package).
71-
plugins.push(createPluginFromDevframe(createInspectDevframe(), {
85+
// `@vitejs/devtools-self-inspect` package). Pinned to a stable id so the
86+
// client can gate it behind the `showDevframeInspector` user setting;
87+
// hidden by default (opt in via Settings → Advanced).
88+
plugins.push(createPluginFromDevframe(createInspectDevframe({ id: DEVTOOLS_INSPECTOR_DOCK_ID }), {
7289
dock: { category: '~builtin', icon: 'ph:stethoscope-duotone' },
7390
}))
7491
}

‎packages/kit/src/constants.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ export const DEVTOOLS_DOCK_IMPORTS_VIRTUAL_ID = '/__devtools-client-imports.js'
4040
*/
4141
export const DEVTOOLS_VITEPLUS_GROUP_ID = '~viteplus'
4242

43+
/**
44+
* Dock id of the built-in Devframe Inspector (mounted from
45+
* `@devframes/plugin-inspect`). Shared between the node side (which pins the
46+
* mounted devframe to this id) and the client (which gates the dock behind the
47+
* `showDevframeInspector` user setting).
48+
*/
49+
export const DEVTOOLS_INSPECTOR_DOCK_ID = 'devframes-plugin-inspect'
50+
4351
export const DEFAULT_CATEGORIES_ORDER: Record<string, number> = {
4452
'default': 0,
4553
'app': 100,

‎packages/kit/src/types/settings.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
1-
export type { DevframeDocksUserSettings as DevToolsDocksUserSettings } from '@devframes/hub/types'
1+
import type { DevframeDocksUserSettings } from '@devframes/hub/types'
2+
3+
/**
4+
* Vite DevTools user settings — the hub's {@link DevframeDocksUserSettings}
5+
* widened with Vite-flavored toggles that only Vite DevTools ships.
6+
*
7+
* The extra fields are optional so the hub's `DEFAULT_STATE_USER_SETTINGS()`
8+
* (which knows nothing about them) stays assignable; an absent value reads as
9+
* its documented default.
10+
*/
11+
export interface DevToolsDocksUserSettings extends DevframeDocksUserSettings {
12+
/**
13+
* Reveal the Devframe Inspector dock (the "DevTools for the DevTools"
14+
* meta-introspection panel). Hidden by default — an absent value keeps the
15+
* dock out of the dock bar until the user opts in from Settings → Advanced.
16+
*/
17+
showDevframeInspector?: boolean
18+
}

‎test/__snapshots__/tsnapi/@vitejs/devtools-kit/constants.snapshot.d.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
export declare const DEFAULT_CATEGORIES_ORDER: Record<string, number>;
66
export declare const DEVTOOLS_DIRNAME: string;
77
export declare const DEVTOOLS_DOCK_IMPORTS_VIRTUAL_ID: string;
8+
export declare const DEVTOOLS_INSPECTOR_DOCK_ID: string;
89
export declare const DEVTOOLS_MOUNT_PATH: string;
910
export declare const DEVTOOLS_MOUNT_PATH_NO_TRAILING_SLASH: string;
1011
export declare const DEVTOOLS_VITEPLUS_GROUP_ID: string;

0 commit comments

Comments
 (0)