From 8bb67fdee2d7fbc3dee014e4a1fb4167a263e11a Mon Sep 17 00:00:00 2001
From: Kacper Wojciechowski <39823706+jog1t@users.noreply.github.com>
Date: Fri, 27 Feb 2026 22:22:34 +0100
Subject: [PATCH] feat: dev toolbar
---
frontend/package.json | 1 +
frontend/src/app/dev-toolbar.tsx | 179 +++++++++++++++++++++++++++++++
frontend/src/routes/__root.tsx | 22 +++-
package.json | 2 +-
pnpm-lock.yaml | 63 +++++++----
5 files changed, 243 insertions(+), 24 deletions(-)
create mode 100644 frontend/src/app/dev-toolbar.tsx
diff --git a/frontend/package.json b/frontend/package.json
index 9f8f0b451a..035a0291b7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -80,6 +80,7 @@
"@tailwindcss/typography": "^0.5.16",
"@tanstack/history": "^1.133.28",
"@tanstack/query-core": "^5.87.1",
+ "@tanstack/react-hotkeys": "^0.3.0",
"@tanstack/react-query": "^5.87.1",
"@tanstack/react-query-devtools": "^5.87.3",
"@tanstack/react-router": "^1.167.1",
diff --git a/frontend/src/app/dev-toolbar.tsx b/frontend/src/app/dev-toolbar.tsx
new file mode 100644
index 0000000000..b08acb1526
--- /dev/null
+++ b/frontend/src/app/dev-toolbar.tsx
@@ -0,0 +1,179 @@
+import { useAuth, useOrganization, useUser } from "@clerk/clerk-react";
+import * as Sentry from "@sentry/react";
+import {
+ formatForDisplay,
+ type Hotkey,
+ useHotkeySequence,
+ useKeyHold,
+} from "@tanstack/react-hotkeys";
+import { useQueryClient } from "@tanstack/react-query";
+import { usePostHog } from "posthog-js/react";
+import { useEffect, useState } from "react";
+import { Kbd, ls } from "@/components";
+
+export const DevToolbar = () => {
+ if (__APP_TYPE__ !== "cloud") return null;
+ if (
+ ls.get(
+ "__I_SOLELY_SWORE_TO_ONLY_ENABLE_DEV_TOOLBAR_FOR_DEBUGGING_PURPOSES_AND_WILL_NOT_USE_IT_FOR_ANY_MALICIOUS_ACTIVITIES__",
+ ) !== true
+ ) {
+ return null;
+ }
+
+ return ;
+};
+
+const debugSequence: Hotkey = "Mod+Shift+G";
+const posthogDebugSequence: Hotkey[] = [debugSequence, "R"];
+const posthogStartRecordingSequence: Hotkey[] = [...posthogDebugSequence, "S"];
+const posthogStopRecordingSequence: Hotkey[] = [...posthogDebugSequence, "T"];
+const posthogLoadToolbarSequence: Hotkey[] = [...posthogDebugSequence, "L"];
+const clearCacheSequence: Hotkey[] = [debugSequence, "C"];
+const reportIssueSequence: Hotkey[] = [debugSequence, "I"];
+
+const Content = () => {
+ const { userId, actor } = useAuth();
+ const { user } = useUser();
+ const { organization: org } = useOrganization();
+
+ const [, setState] = useState({}); // used just to trigger re-render every second
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setState({});
+ }, 1000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const posthog = usePostHog();
+ const queryClient = useQueryClient();
+
+ // recording start
+ useHotkeySequence(posthogStartRecordingSequence, () => {
+ posthog.startSessionRecording();
+ });
+ // recording stop
+ useHotkeySequence(posthogStopRecordingSequence, () => {
+ posthog.stopSessionRecording();
+ });
+ // load toolbar
+ useHotkeySequence(posthogLoadToolbarSequence, () => {
+ posthog.toolbar.loadToolbar();
+ });
+ // clear cache
+ useHotkeySequence(clearCacheSequence, () => {
+ queryClient.clear();
+ });
+ // report issue
+ useHotkeySequence(reportIssueSequence, () => {
+ Sentry.showReportDialog();
+ });
+
+ return (
+
+
+
+ {__APP_TYPE__}{" "}
+ {__APP_BUILD_ID__}
+
+
+
+ {actor ? (
+
+ {actor?.sub || "Unknown"} is signed in as{" "}
+ {userId || "Unknown"}
+
+ ) : null}
+
+ {user?.primaryEmailAddress?.emailAddress || "Unknown"}
+
+
+ {user?.id || "Unknown"} {org?.id || "Unknown"}
+
+
+
+
+
+
rec {posthog.sessionRecording?.status}
+
+ {" "}
+
+
{" "}
+
{" "}
+
{" "}
+ ⋅{" "}
+
{" "}
+
+
+
+
+
+ {" "}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+function ShortcutBadge({ hotkey }: { hotkey: Hotkey | (string & {}) }) {
+ const isShiftHeld = useKeyHold("Shift");
+ if (isShiftHeld) {
+ return {formatForDisplay(hotkey)};
+ }
+ return null;
+}
+
+const Sep = () => ⋅;
diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx
index df1f7ef749..188cc80ba6 100644
--- a/frontend/src/routes/__root.tsx
+++ b/frontend/src/routes/__root.tsx
@@ -1,12 +1,12 @@
import type { Clerk } from "@clerk/clerk-js";
import type { QueryClient } from "@tanstack/react-query";
import {
- Outlet,
createRootRouteWithContext,
+ Outlet,
useNavigate,
} from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
-import { type PropsWithChildren, Suspense, lazy } from "react";
+import { lazy, type PropsWithChildren, Suspense } from "react";
import { match } from "ts-pattern";
import type {
CloudContext,
@@ -16,6 +16,7 @@ import type {
OrganizationContext,
ProjectContext,
} from "@/app/data-providers/cache";
+import { DevToolbar } from "@/app/dev-toolbar";
import { FullscreenLoading } from "@/components";
import { clerkPromise } from "@/lib/auth";
import { cloudEnv } from "@/lib/env";
@@ -24,6 +25,7 @@ function RootRoute() {
return (
<>
+
{import.meta.env.DEV ? (
) : null}
@@ -37,15 +39,24 @@ const LazyClerkProvider = lazy(() =>
import("@clerk/themes"),
clerkPromise,
]).then(([{ ClerkProvider }, { dark }, clerk]) => ({
- default: ({ children, navigatePush, navigateReplace }: PropsWithChildren<{ navigatePush: (to: string) => void; navigateReplace: (to: string) => void }>) => (
+ default: ({
+ children,
+ navigatePush,
+ navigateReplace,
+ }: PropsWithChildren<{
+ navigatePush: (to: string) => void;
+ navigateReplace: (to: string) => void;
+ }>) => (
navigate({ to, replace: true })}
>
+
{import.meta.env.DEV ? (
) : null}
diff --git a/package.json b/package.json
index 951f504a7c..0f9513b585 100644
--- a/package.json
+++ b/package.json
@@ -42,7 +42,7 @@
"@rivetkit/engine-cli": "workspace:*",
"@types/react": "^19",
"@types/react-dom": "^19",
- "@clerk/shared": "3.27.1"
+ "@clerk/shared": "^3.27.1"
},
"pnpm": {
"overrides": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0b42b865e3..78dc48c40e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,7 +15,7 @@ overrides:
'@rivetkit/engine-cli': workspace:*
'@types/react': ^19
'@types/react-dom': ^19
- '@clerk/shared': 3.27.1
+ '@clerk/shared': ^3.27.1
react: 19.1.0
react-dom: 19.1.0
'@rivet-gg/cloud': https://pkg.pr.new/rivet-dev/cloud/@rivet-gg/cloud@413534c
@@ -1388,22 +1388,6 @@ importers:
specifier: ^3.1.1
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@3.1.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.12.10(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)
- examples/global-smoke-test:
- dependencies:
- rivetkit:
- specifier: workspace:*
- version: link:../../rivetkit-typescript/packages/rivetkit
- devDependencies:
- '@types/node':
- specifier: ^22.13.9
- version: 22.19.15
- tsx:
- specifier: ^4.7.1
- version: 4.21.0
- typescript:
- specifier: ^5.5.2
- version: 5.9.3
-
examples/hello-world:
dependencies:
'@rivetkit/react':
@@ -3407,7 +3391,7 @@ importers:
specifier: ^0.23.63
version: 0.23.63(@types/react-dom@19.2.3(@types/react@19.2.13))(@types/react@19.2.13)(next@15.5.9(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@clerk/shared':
- specifier: 3.27.1
+ specifier: ^3.27.1
version: 3.27.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@clerk/themes':
specifier: ^2.4.18
@@ -3565,6 +3549,9 @@ importers:
'@tanstack/query-core':
specifier: ^5.87.1
version: 5.87.1
+ '@tanstack/react-hotkeys':
+ specifier: ^0.3.0
+ version: 0.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/react-query':
specifier: ^5.87.1
version: 5.87.1(react@19.1.0)
@@ -10764,12 +10751,23 @@ packages:
resolution: {integrity: sha512-g9rWt2rNC0Ztga4LAFWfBDT0VREij5mxBo+oH5+cQ5Q46HVIhtJNty/UU5wPzbTKMHJHGNJOWgrlEfcQ4GgjEA==}
engines: {node: '>=20.19'}
+ '@tanstack/hotkeys@0.3.0':
+ resolution: {integrity: sha512-y2uawGLj/GrMNDffaaC0YS7tZPwchDbWAKnU/XObjtyQ4oRGVLyg/2PPbT/hWNQ2PbbrolB098NvlYM0OR3XSw==}
+ engines: {node: '>=18'}
+
'@tanstack/query-core@5.87.1':
resolution: {integrity: sha512-HOFHVvhOCprrWvtccSzc7+RNqpnLlZ5R6lTmngb8aq7b4rc2/jDT0w+vLdQ4lD9bNtQ+/A4GsFXy030Gk4ollA==}
'@tanstack/query-devtools@5.87.3':
resolution: {integrity: sha512-LkzxzSr2HS1ALHTgDmJH5eGAVsSQiuwz//VhFW5OqNk0OQ+Fsqba0Tsf+NzWRtXYvpgUqwQr4b2zdFZwxHcGvg==}
+ '@tanstack/react-hotkeys@0.3.0':
+ resolution: {integrity: sha512-DkSu+8lDwUGzZLk1WVgT6XtfxTa7mFWOGmiDYK+2pe7GqbnuQgUIQPRdo82yCuHYzNoEJgjoYo/HJ8frD4mmcA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ react: 19.1.0
+ react-dom: 19.1.0
+
'@tanstack/react-query-devtools@5.87.3':
resolution: {integrity: sha512-uV7m4/m58jU4OaLEyiPLRoXnL5H5E598lhFLSXIcK83on+ZXW7aIfiu5kwRwe1qFa4X4thH8wKaxz1lt6jNmAA==}
peerDependencies:
@@ -10806,6 +10804,12 @@ packages:
react: 19.1.0
react-dom: 19.1.0
+ '@tanstack/react-store@0.9.1':
+ resolution: {integrity: sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA==}
+ peerDependencies:
+ react: 19.1.0
+ react-dom: 19.1.0
+
'@tanstack/react-store@0.9.2':
resolution: {integrity: sha512-Vt5usJE5sHG/cMechQfmwvwne6ktGCELe89Lmvoxe3LKRoFrhPa8OCKWs0NliG8HTJElEIj7PLtaBQIcux5pAQ==}
peerDependencies:
@@ -10883,6 +10887,9 @@ packages:
'@tanstack/store@0.7.5':
resolution: {integrity: sha512-qd/OjkjaFRKqKU4Yjipaen/EOB9MyEg6Wr9fW103RBPACf1ZcKhbhcu2S5mj5IgdPib6xFIgCUti/mKVkl+fRw==}
+ '@tanstack/store@0.9.1':
+ resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==}
+
'@tanstack/store@0.9.2':
resolution: {integrity: sha512-K013lUJEFJK2ofFQ/hZKJUmCnpcV00ebLyOyFOWQvyQHUOZp/iYO84BM6aOGiV81JzwbX0APTVmW8YI7yiG5oA==}
@@ -27033,10 +27040,21 @@ snapshots:
'@tanstack/history@1.161.5': {}
+ '@tanstack/hotkeys@0.3.0':
+ dependencies:
+ '@tanstack/store': 0.9.1
+
'@tanstack/query-core@5.87.1': {}
'@tanstack/query-devtools@5.87.3': {}
+ '@tanstack/react-hotkeys@0.3.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@tanstack/hotkeys': 0.3.0
+ '@tanstack/react-store': 0.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+
'@tanstack/react-query-devtools@5.87.3(@tanstack/react-query@5.87.1(react@19.1.0))(react@19.1.0)':
dependencies:
'@tanstack/query-devtools': 5.87.3
@@ -27077,6 +27095,13 @@ snapshots:
react-dom: 19.1.0(react@19.1.0)
use-sync-external-store: 1.6.0(react@19.1.0)
+ '@tanstack/react-store@0.9.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@tanstack/store': 0.9.1
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ use-sync-external-store: 1.6.0(react@19.1.0)
+
'@tanstack/react-store@0.9.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@tanstack/store': 0.9.2
@@ -27179,6 +27204,8 @@ snapshots:
'@tanstack/store@0.7.5': {}
+ '@tanstack/store@0.9.1': {}
+
'@tanstack/store@0.9.2': {}
'@tanstack/table-core@8.21.3': {}