diff --git a/components/Providers.tsx b/components/Providers.tsx
index 7567e0a7..0247fbe6 100644
--- a/components/Providers.tsx
+++ b/components/Providers.tsx
@@ -13,29 +13,9 @@ import { ShortcutHelpProvider } from "@/lib/context/ShortcutHelpContext";
import LayoutWrapper from "@/components/LayoutWrapper";
import ToastRegion from "@/components/ToastRegion";
import SessionExpiryProvider from "@/components/SessionExpiryProvider";
-import ShortcutHelpModal from "@/components/ShortcutHelpModal";
-import UnhandledRejectionListener from "@/components/UnhandledRejectionListener";
-import { apiClient } from "@/lib/client/apiClient";
-
-/**
- * Lazy-loaded: CommandPalette pulls in its own icon set (13 lucide-react
- * icons) and renders `null` until the user opens it (Cmd/Ctrl+K), but was
- * previously a static import here -- mounted on every route via `Providers`,
- * its icons shipped in the initial bundle on every page load regardless of
- * whether the palette was ever opened.
- */
-const CommandPalette = lazy(() => import("@/components/CommandPalette"));
-
-/** Keeps the API client's authorization header aligned with wallet state. */
-function ApiClientAuthBridge() {
- const { account, isConnected } = useWallet();
-
- useEffect(() => {
- apiClient.setAuthToken(isConnected ? account?.address : null);
- }, [account?.address, isConnected]);
-
- return null;
-}
+import CommandPalette from "@/components/CommandPalette";
+import DevRequestIdDisplay from "@/components/DevRequestIdDisplay";
+import DevResetHandler from "@/components/dev/DevResetHandler";
/**
* Client-side provider boundary for the app.
@@ -56,14 +36,11 @@ export default function Providers({ children }: { children: ReactNode }) {
-
- {children}
-
-
-
-
-
-
+ {children}
+
+
+
+
diff --git a/components/dev/DevResetHandler.tsx b/components/dev/DevResetHandler.tsx
new file mode 100644
index 00000000..bc602bea
--- /dev/null
+++ b/components/dev/DevResetHandler.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { useEffect, Suspense } from "react";
+import { useRouter, useSearchParams, usePathname } from "next/navigation";
+import { DEV_RESET_QUERY_PARAM, resetLocalStorage } from "@/lib/dev/resetLocalStorage";
+
+/** Watches for `?dev-reset` (added by visiting the URL the `npm run
+ * dev:reset` script prints), clears every app-owned `localStorage` key,
+ * then strips the param so a page refresh doesn't clear storage again. */
+function DevResetHandlerInner() {
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const pathname = usePathname();
+
+ useEffect(() => {
+ if (!searchParams.has(DEV_RESET_QUERY_PARAM)) return;
+
+ resetLocalStorage();
+
+ const remaining = new URLSearchParams(searchParams);
+ remaining.delete(DEV_RESET_QUERY_PARAM);
+ const query = remaining.toString();
+ router.replace(query ? `${pathname}?${query}` : pathname);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [searchParams]);
+
+ return null;
+}
+
+export default function DevResetHandler() {
+ return (
+
+
+
+ );
+}
diff --git a/lib/dev/resetLocalStorage.ts b/lib/dev/resetLocalStorage.ts
new file mode 100644
index 00000000..79b0fd45
--- /dev/null
+++ b/lib/dev/resetLocalStorage.ts
@@ -0,0 +1,31 @@
+import { THEME_STORAGE_KEY } from "@/lib/config/theme";
+import { DEV_MODE_STORAGE_KEY, DEV_MODE_LATEST_REQUEST_ID_KEY } from "@/lib/config/developer";
+
+/** Query param that triggers `resetLocalStorage()` on load -- see
+ * `components/dev/DevResetHandler.tsx`. Paired with the `npm run dev:reset`
+ * script, which just prints the URL to visit (a Node script has no way to
+ * reach into a running browser's `localStorage` directly). */
+export const DEV_RESET_QUERY_PARAM = "dev-reset";
+
+/** Every `localStorage` key this app owns. Kept as one list so `dev:reset`
+ * clears the whole set instead of whichever ones someone remembered --
+ * add new app-level persisted keys here when they're introduced. */
+export const DEV_RESET_LOCAL_STORAGE_KEYS = [
+ THEME_STORAGE_KEY,
+ "display-density",
+ "remitwise_whats_new_last_seen",
+ DEV_MODE_STORAGE_KEY,
+ DEV_MODE_LATEST_REQUEST_ID_KEY,
+] as const;
+
+/** Clears every key in {@link DEV_RESET_LOCAL_STORAGE_KEYS} so a developer
+ * can get back to a fresh first-visit client state (theme, density,
+ * "what's new" seen-state, dev mode) without manually clearing browser
+ * storage. Safe to call on the server -- it's a no-op without `window`. */
+export function resetLocalStorage(): void {
+ if (typeof window === "undefined") return;
+
+ for (const key of DEV_RESET_LOCAL_STORAGE_KEYS) {
+ window.localStorage.removeItem(key);
+ }
+}
diff --git a/package.json b/package.json
index 1978f836..a357d0e7 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"generate:types": "openapi-typescript ./openapi.yaml --output ./src/api/types.ts",
"prebuild": "npm run generate:types",
"dev": "next dev",
- "mock:anchor": "node scripts/mock-anchor-server.mjs",
+ "dev:reset": "node scripts/dev-reset.mjs",
"build": "next build --webpack",
"start": "next start",
"check:img-alt": "node scripts/check-img-alt.js",
diff --git a/scripts/dev-reset.mjs b/scripts/dev-reset.mjs
new file mode 100644
index 00000000..0dcc1489
--- /dev/null
+++ b/scripts/dev-reset.mjs
@@ -0,0 +1,9 @@
+#!/usr/bin/env node
+// Prints the URL that clears the app's localStorage. A Node script has no
+// way to reach into a running browser's storage directly, so `dev:reset`
+// points you at the client-side handler in components/dev/DevResetHandler.tsx
+// instead of pretending to do the clearing itself.
+const port = process.env.PORT || 3000;
+const url = `http://localhost:${port}/?dev-reset`;
+
+console.log(`\nVisit this URL in your browser to clear the app's localStorage:\n\n ${url}\n`);
diff --git a/tests/unit/dev/resetLocalStorage.test.ts b/tests/unit/dev/resetLocalStorage.test.ts
new file mode 100644
index 00000000..066bd9ae
--- /dev/null
+++ b/tests/unit/dev/resetLocalStorage.test.ts
@@ -0,0 +1,35 @@
+import { describe, it, expect, beforeEach } from "vitest";
+import {
+ DEV_RESET_LOCAL_STORAGE_KEYS,
+ resetLocalStorage,
+} from "@/lib/dev/resetLocalStorage";
+
+describe("resetLocalStorage", () => {
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it("clears every app-owned localStorage key", () => {
+ for (const key of DEV_RESET_LOCAL_STORAGE_KEYS) {
+ localStorage.setItem(key, "some-value");
+ }
+
+ resetLocalStorage();
+
+ for (const key of DEV_RESET_LOCAL_STORAGE_KEYS) {
+ expect(localStorage.getItem(key)).toBeNull();
+ }
+ });
+
+ it("leaves keys it doesn't own untouched", () => {
+ localStorage.setItem("some-other-apps-key", "keep-me");
+
+ resetLocalStorage();
+
+ expect(localStorage.getItem("some-other-apps-key")).toBe("keep-me");
+ });
+
+ it("does nothing when localStorage already has no matching keys", () => {
+ expect(() => resetLocalStorage()).not.toThrow();
+ });
+});