From ee52dd1a2a066d81723473b35a7e0e296eaf0132 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Thu, 14 May 2026 11:57:20 +0200 Subject: [PATCH] feat(debug): add env-gated dev debug panel --- src/main/ipc/devDebug.ts | 24 +++ src/main/ipc/index.ts | 2 + src/preload/api/devDebug.ts | 8 + src/preload/api/index.ts | 2 + src/preload/api/types.ts | 4 + src/preload/index.ts | 1 + src/renderer/src/app-state/types.ts | 1 + src/renderer/src/app-state/uiShell/slice.ts | 7 + src/renderer/src/app-state/uiShell/types.ts | 6 + src/renderer/src/app/App.tsx | 33 +++- .../src/features/command-palette/types.ts | 3 + .../command-palette/ui/CommandPalette.tsx | 12 ++ .../HeadlessSnapshotProbe/module.tsx | 148 ++++++++++++++++ .../src/features/debug/devModules/registry.ts | 6 + .../src/features/debug/devModules/types.ts | 23 +++ .../src/features/debug/ui/DevDebugPanel.tsx | 166 ++++++++++++++++++ .../workspace/commands/sessionCommands.ts | 12 ++ 17 files changed, 457 insertions(+), 1 deletion(-) create mode 100644 src/main/ipc/devDebug.ts create mode 100644 src/preload/api/devDebug.ts create mode 100644 src/renderer/src/features/debug/devModules/HeadlessSnapshotProbe/module.tsx create mode 100644 src/renderer/src/features/debug/devModules/registry.ts create mode 100644 src/renderer/src/features/debug/devModules/types.ts create mode 100644 src/renderer/src/features/debug/ui/DevDebugPanel.tsx diff --git a/src/main/ipc/devDebug.ts b/src/main/ipc/devDebug.ts new file mode 100644 index 000000000..30ab6174c --- /dev/null +++ b/src/main/ipc/devDebug.ts @@ -0,0 +1,24 @@ +import { ipcMain } from 'electron' + +export type DevDebugConfig = { + enabled: boolean +} + +function envFlag(name: string): boolean { + const value = process.env[name] + return value === '1' || value === 'true' || value === 'yes' +} + +export function registerDevDebugIpc(): void { + ipcMain.handle('dev-debug:get-config', (): DevDebugConfig => { + return { + // WHY this flag lives in main instead of import.meta.env: + // dev-debug modules are allowed to be noisy, temporary, and + // sometimes performance-hostile. Gating them from the same + // project-root `.env` loader as performance telemetry gives us a + // runtime switch that works in Electron dev without requiring a + // Vite-prefixed renderer variable or rebuild-time config. + enabled: envFlag('AGENT_CODE_DEV_DEBUG'), + } + }) +} diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 11d7a7631..961c796c4 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -18,6 +18,7 @@ import { registerSetupIpc } from '@main/ipc/setup.js' import { registerWorktreeActivityIpc } from '@main/ipc/worktreeActivity.js' import { registerDictationIpc } from '@main/ipc/dictation.js' import { registerPasteDebugIpc } from '@main/ipc/pasteDebug.js' +import { registerDevDebugIpc } from '@main/ipc/devDebug.js' import { installPerformanceIpcInstrumentation } from '@main/performance/instrumentIpc.js' import type { WorktreeActivityIndex } from '@main/worktreeActivity/WorktreeActivityIndex.js' @@ -55,4 +56,5 @@ export function registerAllIpc(deps: IpcDeps): void { registerSetupIpc() registerDictationIpc({ dictationDebugJournals: deps.dictationDebugJournals }) registerPasteDebugIpc({ pasteDebugJournals: deps.pasteDebugJournals }) + registerDevDebugIpc() } diff --git a/src/preload/api/devDebug.ts b/src/preload/api/devDebug.ts new file mode 100644 index 000000000..4aa0fe3a6 --- /dev/null +++ b/src/preload/api/devDebug.ts @@ -0,0 +1,8 @@ +import { ipcRenderer } from 'electron' + +import type { DevDebugConfig } from '@preload/api/types.js' + +export const devDebugApi = { + getDevDebugConfig: (): Promise => + ipcRenderer.invoke('dev-debug:get-config'), +} diff --git a/src/preload/api/index.ts b/src/preload/api/index.ts index 98651187d..cee4e18b7 100644 --- a/src/preload/api/index.ts +++ b/src/preload/api/index.ts @@ -13,6 +13,7 @@ import { setupApi } from '@preload/api/setup.js' import { dictationApi } from '@preload/api/dictation.js' import { dictationDebugApi } from '@preload/api/dictationDebug.js' import { pasteDebugApi } from '@preload/api/pasteDebug.js' +import { devDebugApi } from '@preload/api/devDebug.js' // Composed preload API surface. // @@ -45,6 +46,7 @@ export const api = { ...dictationApi, ...dictationDebugApi, ...pasteDebugApi, + ...devDebugApi, } export type Api = typeof api diff --git a/src/preload/api/types.ts b/src/preload/api/types.ts index d0076ca39..74c1c480f 100644 --- a/src/preload/api/types.ts +++ b/src/preload/api/types.ts @@ -14,6 +14,10 @@ export type { ProviderConditionSnapshot } from '@shared/types/providerConditions export type Unsub = () => void +export type DevDebugConfig = { + enabled: boolean +} + export type DictationProvider = 'deepgram' | 'assemblyai' | 'openai' | 'gladia' | 'elevenlabs' export type DictationStartResult = diff --git a/src/preload/index.ts b/src/preload/index.ts index a7b68dbca..96c795419 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -30,6 +30,7 @@ export type { Api } // preload bundle. export type { FeedDebugPersistEntry, + DevDebugConfig, JsonlEntry, LspDiagnostic, LspDiagnosticsEvent, diff --git a/src/renderer/src/app-state/types.ts b/src/renderer/src/app-state/types.ts index 1f10cffda..dbabf44cc 100644 --- a/src/renderer/src/app-state/types.ts +++ b/src/renderer/src/app-state/types.ts @@ -47,6 +47,7 @@ export type UiShellSlice = UiShellState & { toggleFeedDebugPanel: () => void toggleProxyDebugPanel: () => void toggleHtmlDebugPanel: () => void + toggleDevDebugPanel: () => void togglePerformancePanel: () => void openPromptSearch: () => void closePromptSearch: () => void diff --git a/src/renderer/src/app-state/uiShell/slice.ts b/src/renderer/src/app-state/uiShell/slice.ts index 04919073f..b4c21e9ea 100644 --- a/src/renderer/src/app-state/uiShell/slice.ts +++ b/src/renderer/src/app-state/uiShell/slice.ts @@ -26,6 +26,7 @@ export const createUiShellSlice: StateCreator< feedDebugPanelOpen: false, proxyDebugPanelOpen: false, htmlDebugPanelOpen: false, + devDebugPanelOpen: false, performancePanelOpen: false, promptSearchOpen: false, agentActivityOpen: false, @@ -119,6 +120,12 @@ export const createUiShellSlice: StateCreator< false, 'uiShell/toggleHtmlDebugPanel', ), + toggleDevDebugPanel: () => + set( + state => ({ devDebugPanelOpen: !state.devDebugPanelOpen }), + false, + 'uiShell/toggleDevDebugPanel', + ), togglePerformancePanel: () => set( state => ({ performancePanelOpen: !state.performancePanelOpen }), diff --git a/src/renderer/src/app-state/uiShell/types.ts b/src/renderer/src/app-state/uiShell/types.ts index 6a7c0c978..da9eaf66f 100644 --- a/src/renderer/src/app-state/uiShell/types.ts +++ b/src/renderer/src/app-state/uiShell/types.ts @@ -62,6 +62,12 @@ export type UiShellState = { * Lives on the uiShell slice alongside the other three debug panels * and follows the same toggle pattern. */ htmlDebugPanelOpen: boolean + /** When true, the .env-gated Dev Debug Panel is mounted. Unlike the + * stable debug panels above, this is a temporary module host for + * one-off investigations. Its modules are intentionally freeform: + * they may poll, render custom UI, or call IPC while a bug is being + * understood, and then disappear after the fix lands. */ + devDebugPanelOpen: boolean performancePanelOpen: boolean /** When true, the Search Conversation Prompts modal is open. Lives * on the uiShell slice (not the workspace slice) because it's diff --git a/src/renderer/src/app/App.tsx b/src/renderer/src/app/App.tsx index d960c6121..ace5effb1 100644 --- a/src/renderer/src/app/App.tsx +++ b/src/renderer/src/app/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { CommandPalette } from '@renderer/features/command-palette/ui/CommandPalette' import { PinAgentsModal, type PinAgentsModalRow } from '@renderer/features/dispatch-pin/PinAgentsModal' @@ -6,6 +6,7 @@ import { DebugPanel } from '@renderer/features/debug/ui/DebugPanel' import { FeedDebugPanel } from '@renderer/features/debug/ui/FeedDebugPanel' import { HtmlDebugPanel } from '@renderer/features/debug/ui/HtmlDebugPanel' import { ProxyDebugPanel } from '@renderer/features/debug/ui/ProxyDebugPanel' +import { DevDebugPanel } from '@renderer/features/debug/ui/DevDebugPanel' import { SettingsPage } from '@renderer/features/settings/ui/SettingsPage' import { SetupGate } from '@renderer/features/setup/ui/SetupGate' import { SpotlightView } from '@renderer/features/spotlight/ui/SpotlightView' @@ -78,6 +79,7 @@ export default function App() { const feedDebugPanelOpen = useAppStore(state => state.feedDebugPanelOpen) const proxyDebugPanelOpen = useAppStore(state => state.proxyDebugPanelOpen) const htmlDebugPanelOpen = useAppStore(state => state.htmlDebugPanelOpen) + const devDebugPanelOpen = useAppStore(state => state.devDebugPanelOpen) const performancePanelOpen = useAppStore(state => state.performancePanelOpen) const dangerousAgentsEnabled = settings.dangerousAgentsEnabled const aggressiveDebugPersistenceEnabled = settings.aggressiveDebugPersistence @@ -117,6 +119,7 @@ export default function App() { const toggleFeedDebugPanel = useAppStore(state => state.toggleFeedDebugPanel) const toggleProxyDebugPanel = useAppStore(state => state.toggleProxyDebugPanel) const toggleHtmlDebugPanel = useAppStore(state => state.toggleHtmlDebugPanel) + const toggleDevDebugPanel = useAppStore(state => state.toggleDevDebugPanel) const togglePerformancePanel = useAppStore(state => state.togglePerformancePanel) const promptSearchOpen = useAppStore(state => state.promptSearchOpen) const openPromptSearch = useAppStore(state => state.openPromptSearch) @@ -127,11 +130,26 @@ export default function App() { const rewindPromptSessionId = useAppStore(state => state.rewindPromptSessionId) const openRewindPrompt = useAppStore(state => state.openRewindPrompt) const closeRewindPrompt = useAppStore(state => state.closeRewindPrompt) + const [devDebugEnabled, setDevDebugEnabled] = useState(false) useEffect(() => { applyTheme(settings) }, [settings]) + useEffect(() => { + let cancelled = false + void window.api.getDevDebugConfig() + .then(config => { + if (!cancelled) setDevDebugEnabled(config.enabled) + }) + .catch(() => { + if (!cancelled) setDevDebugEnabled(false) + }) + return () => { + cancelled = true + } + }, []) + useEffect(() => { // The default dictation trigger is bare Fn, which Chromium does not expose // reliably to renderer keydown. Settings live in the renderer, but the @@ -529,6 +547,16 @@ export default function App() { onClose={toggleHtmlDebugPanel} /> )} + + {devDebugEnabled && devDebugPanelOpen && commandTargetId && ( + + )} @@ -574,6 +603,8 @@ export default function App() { feedDebugPanelOpen={feedDebugPanelOpen} proxyDebugPanelOpen={proxyDebugPanelOpen} htmlDebugPanelOpen={htmlDebugPanelOpen} + devDebugEnabled={devDebugEnabled} + devDebugPanelOpen={devDebugPanelOpen} performancePanelOpen={performancePanelOpen} dispatchModeEnabled={workspace.dispatchMode !== null} globalDispatchEnabled={workspace.dispatchMode?.scope === 'global'} diff --git a/src/renderer/src/features/command-palette/types.ts b/src/renderer/src/features/command-palette/types.ts index e88f51a4a..7b8c8c10e 100644 --- a/src/renderer/src/features/command-palette/types.ts +++ b/src/renderer/src/features/command-palette/types.ts @@ -23,6 +23,7 @@ export type CommandContext = { toggleFeedDebugPanel: () => void toggleProxyDebugPanel: () => void toggleHtmlDebugPanel: () => void + toggleDevDebugPanel: () => void togglePerformancePanel: () => void enterDispatchMode: () => Promise | void enterGlobalDispatch: () => Promise | void @@ -60,6 +61,8 @@ export type CommandContext = { feedDebugPanelOpen: boolean proxyDebugPanelOpen: boolean htmlDebugPanelOpen: boolean + devDebugEnabled: boolean + devDebugPanelOpen: boolean performancePanelOpen: boolean dispatchModeEnabled: boolean globalDispatchEnabled: boolean diff --git a/src/renderer/src/features/command-palette/ui/CommandPalette.tsx b/src/renderer/src/features/command-palette/ui/CommandPalette.tsx index 10f76cae5..96c4fd970 100644 --- a/src/renderer/src/features/command-palette/ui/CommandPalette.tsx +++ b/src/renderer/src/features/command-palette/ui/CommandPalette.tsx @@ -73,6 +73,7 @@ type Props = { toggleFeedDebugPanel: () => void toggleProxyDebugPanel: () => void toggleHtmlDebugPanel: () => void + toggleDevDebugPanel: () => void togglePerformancePanel: () => void enterDispatchMode: () => Promise | void enterGlobalDispatch: () => Promise | void @@ -93,6 +94,8 @@ type Props = { feedDebugPanelOpen: boolean proxyDebugPanelOpen: boolean htmlDebugPanelOpen: boolean + devDebugEnabled: boolean + devDebugPanelOpen: boolean performancePanelOpen: boolean dispatchModeEnabled: boolean globalDispatchEnabled: boolean @@ -129,6 +132,7 @@ export function CommandPalette({ toggleFeedDebugPanel, toggleProxyDebugPanel, toggleHtmlDebugPanel, + toggleDevDebugPanel, togglePerformancePanel, enterDispatchMode, enterGlobalDispatch, @@ -149,6 +153,8 @@ export function CommandPalette({ feedDebugPanelOpen, proxyDebugPanelOpen, htmlDebugPanelOpen, + devDebugEnabled, + devDebugPanelOpen, performancePanelOpen, dispatchModeEnabled, globalDispatchEnabled, @@ -289,6 +295,7 @@ export function CommandPalette({ toggleFeedDebugPanel, toggleProxyDebugPanel, toggleHtmlDebugPanel, + toggleDevDebugPanel, togglePerformancePanel, enterDispatchMode, enterGlobalDispatch, @@ -319,6 +326,8 @@ export function CommandPalette({ feedDebugPanelOpen, proxyDebugPanelOpen, htmlDebugPanelOpen, + devDebugEnabled, + devDebugPanelOpen, performancePanelOpen, dispatchModeEnabled, globalDispatchEnabled, @@ -341,6 +350,7 @@ export function CommandPalette({ toggleFeedDebugPanel, toggleProxyDebugPanel, toggleHtmlDebugPanel, + toggleDevDebugPanel, togglePerformancePanel, enterDispatchMode, enterGlobalDispatch, @@ -369,6 +379,8 @@ export function CommandPalette({ feedDebugPanelOpen, proxyDebugPanelOpen, htmlDebugPanelOpen, + devDebugEnabled, + devDebugPanelOpen, performancePanelOpen, dispatchModeEnabled, globalDispatchEnabled, diff --git a/src/renderer/src/features/debug/devModules/HeadlessSnapshotProbe/module.tsx b/src/renderer/src/features/debug/devModules/HeadlessSnapshotProbe/module.tsx new file mode 100644 index 000000000..2c5965cdf --- /dev/null +++ b/src/renderer/src/features/debug/devModules/HeadlessSnapshotProbe/module.tsx @@ -0,0 +1,148 @@ +import { useMemo, useState } from 'react' + +import type { DevDebugModule, DevDebugModuleProps } from '@renderer/features/debug/devModules/types' + +export const headlessSnapshotProbeModule: DevDebugModule = { + id: 'headless-snapshot-probe', + title: 'Headless Snapshot Probe', + description: 'Inspect focused screen/markdown snapshots and test regexes against both.', + Component: HeadlessSnapshotProbe, +} + +function HeadlessSnapshotProbe({ sessionId, runtime, kind }: DevDebugModuleProps) { + const [pattern, setPattern] = useState('\\[Pasted text #\\d+') + const [flags, setFlags] = useState('i') + const plain = runtime.screen + const markdown = runtime.screenMarkdown + + const regex = useMemo(() => { + try { + return { ok: true as const, value: new RegExp(pattern, flags) } + } catch (err) { + return { + ok: false as const, + message: err instanceof Error ? err.message : String(err), + } + } + }, [flags, pattern]) + + const plainMatch = useMemo( + () => regex.ok ? summarizeMatch(plain, regex.value) : null, + [plain, regex], + ) + const markdownMatch = useMemo( + () => regex.ok ? summarizeMatch(markdown, regex.value) : null, + [markdown, regex], + ) + + return ( +
+
+
+
+ headless snapshot probe +
+
+ {kind} · {sessionId} +
+
+
+ {plain.length} plain / {markdown.length} md +
+
+ +
+
+ + +
+ + {!regex.ok && ( +
+ {regex.message} +
+ )} + + {regex.ok && ( +
+ + +
+ )} + + + +
+
+ ) +} + +function summarizeMatch(value: string, regex: RegExp): { matched: boolean; index: number | null; text: string } { + regex.lastIndex = 0 + const match = regex.exec(value) + if (!match) return { matched: false, index: null, text: '' } + return { + matched: true, + index: match.index, + text: match[0], + } +} + +function MatchCard({ + title, + result, +}: { + title: string + result: { matched: boolean; index: number | null; text: string } | null +}) { + const matched = Boolean(result?.matched) + return ( +
+
+ {title} + + {matched ? 'match' : 'no match'} + +
+
+ {matched ? `@${result?.index}: ${result?.text}` : '(none)'} +
+
+ ) +} + +function Snapshot({ title, value }: { title: string; value: string }) { + return ( +
+
+ {title} + +
+
+        {value}
+      
+
+ ) +} diff --git a/src/renderer/src/features/debug/devModules/registry.ts b/src/renderer/src/features/debug/devModules/registry.ts new file mode 100644 index 000000000..442d66420 --- /dev/null +++ b/src/renderer/src/features/debug/devModules/registry.ts @@ -0,0 +1,6 @@ +import type { DevDebugModule } from '@renderer/features/debug/devModules/types' +import { headlessSnapshotProbeModule } from '@renderer/features/debug/devModules/HeadlessSnapshotProbe/module' + +export const devDebugModules: DevDebugModule[] = [ + headlessSnapshotProbeModule, +] diff --git a/src/renderer/src/features/debug/devModules/types.ts b/src/renderer/src/features/debug/devModules/types.ts new file mode 100644 index 000000000..45637b276 --- /dev/null +++ b/src/renderer/src/features/debug/devModules/types.ts @@ -0,0 +1,23 @@ +import type { Workspace } from '@renderer/workspace/workspaceStore' +import type { SessionRuntime } from '@renderer/workspace/workspaceState' + +export type DevDebugModuleProps = { + workspace: Workspace + sessionId: string + runtime: SessionRuntime + kind: string +} + +export type DevDebugModule = { + id: string + title: string + description?: string + // WHY the contract stops at "render this component": + // debug investigations are not predictable product surfaces. One + // module may need a regex workbench, another may need IPC polling, + // another may need a graph or a custom event buffer. Forcing a + // shared schema or lifecycle API now would make the first weird bug + // fight the host. The panel owns discovery and visibility only; the + // module owns everything specific to its investigation. + Component: React.ComponentType +} diff --git a/src/renderer/src/features/debug/ui/DevDebugPanel.tsx b/src/renderer/src/features/debug/ui/DevDebugPanel.tsx new file mode 100644 index 000000000..156103c41 --- /dev/null +++ b/src/renderer/src/features/debug/ui/DevDebugPanel.tsx @@ -0,0 +1,166 @@ +import { useEffect, useMemo, useState } from 'react' + +import { devDebugModules } from '@renderer/features/debug/devModules/registry' +import type { DevDebugModule } from '@renderer/features/debug/devModules/types' +import type { Workspace } from '@renderer/workspace/workspaceStore' +import type { SessionRuntime } from '@renderer/workspace/workspaceState' + +const STORAGE_KEY = 'agent-code:dev-debug:enabled-modules' + +type Props = { + sessionId: string + runtime: SessionRuntime + kind: string + workspace: Workspace + onClose: () => void +} + +export function DevDebugPanel({ sessionId, runtime, kind, workspace, onClose }: Props) { + const [enabledIds, setEnabledIds] = useState(() => readEnabledIds()) + const moduleIds = useMemo(() => new Set(devDebugModules.map(module => module.id)), []) + + useEffect(() => { + setEnabledIds(prev => { + const next = prev.filter(id => moduleIds.has(id)) + if (next.length !== prev.length) writeEnabledIds(next) + return next + }) + }, [moduleIds]) + + const enabledSet = useMemo(() => new Set(enabledIds), [enabledIds]) + const enabledModules = useMemo( + () => devDebugModules.filter(module => enabledSet.has(module.id)), + [enabledSet], + ) + + const toggleModule = (module: DevDebugModule) => { + setEnabledIds(prev => { + const next = prev.includes(module.id) + ? prev.filter(id => id !== module.id) + : [...prev, module.id] + writeEnabledIds(next) + return next + }) + } + + return ( +
+
+ dev debug + +
+ +
+
+
+
+ focused session +
+
+ {kind} · {sessionId} +
+
+
+ {enabledModules.length}/{devDebugModules.length} modules +
+
+
+ +
+ {devDebugModules.length === 0 ? ( +
no dev debug modules registered
+ ) : ( +
+ {devDebugModules.map(module => { + const enabled = enabledSet.has(module.id) + return ( + + ) + })} +
+ )} +
+ +
+ {enabledModules.length === 0 ? ( +
+ no dev debug modules enabled +
+ ) : ( + enabledModules.map(module => ( + + )) + )} +
+
+ ) +} + +function readEnabledIds(): string[] { + try { + const raw = window.localStorage.getItem(STORAGE_KEY) + if (!raw) return [] + const parsed = JSON.parse(raw) + if (!Array.isArray(parsed)) return [] + return parsed.filter((item): item is string => typeof item === 'string') + } catch { + return [] + } +} + +function writeEnabledIds(ids: string[]): void { + try { + // WHY only ids are persisted: module state is investigation-local. + // A regex probe may want one storage key, a future submit timeline + // may want another, and many modules should leave no residue at + // all. The host persists visibility only so deleting a temporary + // module does not strand a schema-shaped blob in app settings. + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(ids)) + } catch { + // localStorage can throw in restricted environments; losing a + // debug-panel toggle is not worth surfacing to the product UI. + } +} diff --git a/src/renderer/src/features/workspace/commands/sessionCommands.ts b/src/renderer/src/features/workspace/commands/sessionCommands.ts index d2dac1b17..b40045b9e 100644 --- a/src/renderer/src/features/workspace/commands/sessionCommands.ts +++ b/src/renderer/src/features/workspace/commands/sessionCommands.ts @@ -395,4 +395,16 @@ export const sessionCommands: CommandDef[] = [ }), run: ({ ui }) => ui.toggleHtmlDebugPanel(), }, + { + id: 'toggle-dev-debug-panel', + title: 'Dev Debug Panel', + description: '**What it does:** Shows or hides the temporary **Dev Debug Panel** module host.\n\n**Use when:** You need a bug-specific workbench for focused runtime state, regex probes, IPC experiments, or other short-lived diagnostics.\n\n**Notes:** Only appears when `AGENT_CODE_DEV_DEBUG=1` is set.', + keywords: ['dev', 'debug', 'module', 'probe', 'regex', 'headless', 'snapshot', 'temporary'], + when: ({ flags }) => flags.devDebugEnabled, + getState: ({ flags }) => ({ + label: flags.devDebugPanelOpen ? 'On' : 'Off', + tone: flags.devDebugPanelOpen ? 'accent' : 'neutral', + }), + run: ({ ui }) => ui.toggleDevDebugPanel(), + }, ]