From fb4c3ec6d4b4f6eea1c3bcccadad6f6e5772675a Mon Sep 17 00:00:00 2001
From: Danny White <3104761+dnywh@users.noreply.github.com>
Date: Fri, 21 Aug 2026 10:53:50 +1000
Subject: [PATCH] feat(studio): add dev toolbar launcher to account settings
menu (#49285)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## What kind of change does this PR introduce?
Feature
## What is the current behavior?
The dev toolbar is only discoverable via `window.devToolbar()` in the
browser console, or by having your email on the `devToolbarDefaultOn`
ConfigCat flag. Once enabled, Studio shows a floating trigger button.
## What is the new behavior?
In local and staging Studio, the account/settings dropdown (avatar menu)
includes a **Local tools** section above **Theme** with a **Dev
toolbar** checkbox toggle.
- **On**: shows the floating orb (persists via localStorage, same as
`window.devToolbar()`)
- **Off**: hides the orb and dismisses the toolbar
Open the panel itself via the orb once it is visible. Production builds
are unchanged (`isAvailable` is false and the menu item is hidden).
| After |
| --- |
| |
| |
## Manual testing
Run `pnpm dev:studio` and open http://localhost:8082.
1. **Find the entry point:** top-right avatar/settings menu → **Local
tools** → **Dev toolbar** (above **Theme**). Should not appear in
production builds.
2. **Turn it on:** check **Dev toolbar**. A green floating orb should
appear (default bottom-right).
3. **Open the panel:** click the orb. The **Dev Toolbar** sheet should
open with Events and Flags tabs.
4. **Event count:** navigate around Studio (e.g. open a project, switch
pages). The orb badge should increment and stay readable in light and
dark mode.
5. **Turn it off:** reopen the avatar menu and uncheck **Dev toolbar**.
The orb and panel should disappear.
6. **Close vs hide:** with the toolbar on, open the sheet and use
**Close** (X). The orb should remain; only the sheet closes.
Optional: confirm `window.devToolbar()` in the browser console still
enables the orb.
## Summary by CodeRabbit
* **New Features**
* Added a Local tools option to enable the development toolbar when
available.
* Toolbar activation and dismissal preferences now persist between
sessions.
* Added clearer event-count badges with responsive sizing for larger
counts.
* **Improvements**
* Simplified toolbar controls by removing the separate hide option.
* Improved toolbar availability handling across local and production
environments.
* **Tests**
* Expanded coverage for activation, persistence, visibility, and
event-count badges.
---------
Co-authored-by: Cursor Agent
Co-authored-by: Danny White
Co-authored-by: Sean Oliver <882952+seanoliver@users.noreply.github.com>
---
.../interfaces/DevToolbarMenuGroup.tsx | 30 ++++
.../interfaces/LocalDropdown.test.tsx | 122 +++++++++++--
.../components/interfaces/LocalDropdown.tsx | 2 +
.../components/interfaces/UserDropdown.tsx | 9 +-
packages/dev-tools/DevToolbar.test.tsx | 160 ++++++++++++++++++
packages/dev-tools/DevToolbar.tsx | 15 +-
packages/dev-tools/DevToolbarContext.tsx | 26 ++-
packages/dev-tools/DevToolbarTrigger.tsx | 20 +--
packages/dev-tools/index.ts | 2 +
packages/dev-tools/types.ts | 2 +
packages/dev-tools/utils.ts | 14 ++
11 files changed, 357 insertions(+), 45 deletions(-)
create mode 100644 apps/studio/components/interfaces/DevToolbarMenuGroup.tsx
diff --git a/apps/studio/components/interfaces/DevToolbarMenuGroup.tsx b/apps/studio/components/interfaces/DevToolbarMenuGroup.tsx
new file mode 100644
index 00000000000..495c4da8861
--- /dev/null
+++ b/apps/studio/components/interfaces/DevToolbarMenuGroup.tsx
@@ -0,0 +1,30 @@
+import { useDevToolbar } from 'dev-tools'
+import { DropdownMenuCheckboxItem, DropdownMenuGroup, DropdownMenuLabel } from 'ui'
+
+export function DevToolbarMenuGroup() {
+ const { isAvailable, isEnabled, enableToolbar, dismissToolbar } = useDevToolbar()
+
+ if (!isAvailable) return null
+
+ const handleToggleDevToolbar = (isChecked: boolean) => {
+ if (isChecked) {
+ enableToolbar()
+ return
+ }
+
+ dismissToolbar()
+ }
+
+ return (
+
+ Local tools
+
+ Dev toolbar
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/LocalDropdown.test.tsx b/apps/studio/components/interfaces/LocalDropdown.test.tsx
index 5afef5f725f..7cdfdb91a9d 100644
--- a/apps/studio/components/interfaces/LocalDropdown.test.tsx
+++ b/apps/studio/components/interfaces/LocalDropdown.test.tsx
@@ -1,21 +1,41 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import type { MouseEventHandler, ReactElement, ReactNode } from 'react'
-import { describe, expect, it, vi } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
import { LocalDropdown } from './LocalDropdown'
-const { mockRouter, mockSetTheme, mockSetLastRoute, mockToggleFeaturePreviewModal } = vi.hoisted(
- () => ({
- mockRouter: {
- pathname: '/project/[ref]/editor',
- asPath: '/project/default/editor',
- },
- mockSetTheme: vi.fn(),
- mockSetLastRoute: vi.fn(),
- mockToggleFeaturePreviewModal: vi.fn(),
- })
-)
+const {
+ mockRouter,
+ mockSetTheme,
+ mockSetLastRoute,
+ mockToggleFeaturePreviewModal,
+ mockEnableToolbar,
+ mockDismissDevToolbar,
+ mockSetDevToolbarOpen,
+ mockUseDevToolbar,
+} = vi.hoisted(() => ({
+ mockRouter: {
+ pathname: '/project/[ref]/editor',
+ asPath: '/project/default/editor',
+ },
+ mockSetTheme: vi.fn(),
+ mockSetLastRoute: vi.fn(),
+ mockToggleFeaturePreviewModal: vi.fn(),
+ mockEnableToolbar: vi.fn(),
+ mockDismissDevToolbar: vi.fn(),
+ mockSetDevToolbarOpen: vi.fn(),
+ mockUseDevToolbar: vi.fn(() => ({
+ isAvailable: false,
+ isEnabled: false,
+ isOpen: false,
+ setIsOpen: mockSetDevToolbarOpen,
+ enableToolbar: mockEnableToolbar,
+ dismissToolbar: mockDismissDevToolbar,
+ events: [],
+ setEvents: vi.fn(),
+ })),
+}))
vi.mock('next/router', () => ({
useRouter: () => mockRouter,
@@ -62,6 +82,10 @@ vi.mock('./App/FeaturePreview/FeaturePreviewContext', () => ({
vi.mock('@/lib/telemetry/track', () => ({ useTrack: () => vi.fn() }))
+vi.mock('dev-tools', () => ({
+ useDevToolbar: () => mockUseDevToolbar(),
+}))
+
vi.mock('ui', async () => {
const React = await import('react')
@@ -104,6 +128,19 @@ vi.mock('ui', async () => {
),
DropdownMenuLabel: ({ children }: { children: ReactNode }) =>