Repository: gradio-app/gradio
Pull request: Add ability to read and write from LocalStorage
I've added a component `gr.BrowserState()`, which acts similarly to `gr.State()` but any value written to it is saved to the browser's local storage, so it is preserved across page loads. The value is encrypted with a key, which means that one Gradio app will not be able to read the local state value of another Gradio app. 

Test demo: `demo/browserstate/run.py`

Closes: https://github.com/gradio-app/gradio/issues/3106

Problem statement:
Support session state across multiple pages or page refresh (e.g. with LocalStorage)
Right now, the session state is either global shared between users or local within a page. We are seeing the needs to support session state across multiple pages.

E.g., we host a service on Hugging Face spaces, and we ask users to login with token first before using the spaces. The ideal case is users just need to login once but use the spaces multiple time without further login (even the page is refreshed, or multiple pages requires login status).

Hints from issue discussion:
@abidlabs is this something that's on the roadmap?
We’re planning on tackling it but probably not anytime soon as we have competing priorities at the moment
Hi, abidlabs, Is there a workaround to keep chat histroy in `gr.Chatbot` after refreshing a page?
@abidlabs ^ that is my use case as well; curious what your recommended workaround is.
Hi folks! It is possible to use localstorage by using custom javascript for now. Here's a gradio app that does this: https://huggingface.co/spaces/radames/gradio_window_localStorage
Hi @abidlabs,
How can I find the session data associated with the page after the user refreshes it using the above configuration?
I mean, although it can store data, how can i find previous `gr. State`? 


It would be nice to persist this on the backend so that authenticated sessions synchronize across devices also. I suspect this is actually simpler to implement. (I will assume in-memory persistence, because db-like schema migrations add significant complexity.)

Here is my current workaround:

```py
import gradio as gr
from helper import persist

with gr.Blocks() as demo:
    foo_txt = persist(gr.Textbox())
    bar_txt = persist(gr.Textbox())

demo.launch(auth=[("a", "1"), ("b", "2")])
```

<details><summary>helper.py</summary>

```py
from gradio.context import Context
from gradio import Request


def persist(component):
    sessions = {}

    def resume_session(value, request: Request):
        return sessions.get(request.username, value)

    def update_session(value, request: Request):
        sessions[request.username] = value

    Context.root_block.load(resume_session, inputs=[component], outputs=component)
    component.change(update_session, inputs=[component])

    return component
```

</details>

I suppose this could be encapsulated in gradio for the slightly sweeter `gr.Textbox(persist=True)` syntax. A complete solution should probably also detect the auth type and choose the session key accordingly. It is also possible to use request cookies for the session ID so that the UX feels like LocalStorage for anonymous users.
@abidlabs Hey is there any update on this? I think persisting sessions would be a very useful feature and I see this issue is from a year ago.
I agree, cc @aliabid94 perhaps if he has bandwidth. 
+1 to see this happen.

Commit message: types

Patch to review:
diff --git a/.changeset/tricky-glasses-share.md b/.changeset/tricky-glasses-share.md
new file mode 100644
index 0000000000..79bdb03afb
--- /dev/null
+++ b/.changeset/tricky-glasses-share.md
@@ -0,0 +1,12 @@
+---
+"@gradio/client": minor
+"@gradio/core": minor
+"@gradio/browserstate": minor
+"@gradio/utils": minor
+"@self/app": minor
+"@self/component-test": minor
+"@self/spa": minor
+"gradio": minor
+---
+
+feat:Add ability to read and write from LocalStorage
diff --git a/demo/browser_state_component/run.ipynb b/demo/browser_state_component/run.ipynb
new file mode 100644
index 0000000000..104d4f4118
--- /dev/null
+++ b/demo/browser_state_component/run.ipynb
@@ -0,0 +1 @@
+{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: browser_state_component"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", "    gr.BrowserState()\n", "\n", "demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
\ No newline at end of file
diff --git a/demo/browser_state_component/run.py b/demo/browser_state_component/run.py
new file mode 100644
index 0000000000..20999a495f
--- /dev/null
+++ b/demo/browser_state_component/run.py
@@ -0,0 +1,6 @@
+import gradio as gr
+
+with gr.Blocks() as demo:
+    gr.BrowserState()
+
+demo.launch()
diff --git a/demo/browserstate/run.ipynb b/demo/browserstate/run.ipynb
new file mode 100644
index 0000000000..b3fc96115a
--- /dev/null
+++ b/demo/browserstate/run.ipynb
@@ -0,0 +1 @@
+{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: browserstate"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import random\n", "import string\n", "import gradio as gr\n", "\n", "with gr.Blocks() as demo:\n", "    gr.Markdown(\"Your Username and Password will get saved in the browser's local storage. \"\n", "                \"If you refresh the page, the values will be retained.\")\n", "    username = gr.Textbox(label=\"Username\")\n", "    password = gr.Textbox(label=\"Password\", type=\"password\")\n", "    btn = gr.Button(\"Generate Randomly\")\n", "    local_storage = gr.BrowserState([\"\", \"\"])\n", "\n", "    @btn.click(outputs=[username, password])\n", "    def generate_randomly():\n", "        u = \"\".join(random.choices(string.ascii_letters + string.digits, k=10))\n", "        p = \"\".join(random.choices(string.ascii_letters + string.digits, k=10))\n", "        return u, p\n", "\n", "    @demo.load(inputs=[local_storage], outputs=[username, password])\n", "    def load_from_local_storage(saved_values):\n", "        print(\"loading from local storage\", saved_values)\n", "        return saved_values[0], saved_values[1]\n", "\n", "    @gr.on([username.change, password.change], inputs=[username, password], outputs=[local_storage])\n", "    def save_to_local_storage(username, password):\n", "        return [username, password]\n", "\n", "demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}
\ No newline at end of file
diff --git a/demo/browserstate/run.py b/demo/browserstate/run.py
new file mode 100644
index 0000000000..294b56fd58
--- /dev/null
+++ b/demo/browserstate/run.py
@@ -0,0 +1,28 @@
+import random
+import string
+import gradio as gr
+
+with gr.Blocks() as demo:
+    gr.Markdown("Your Username and Password will get saved in the browser's local storage. "
+                "If you refresh the page, the values will be retained.")
+    username = gr.Textbox(label="Username")
+    password = gr.Textbox(label="Password", type="password")
+    btn = gr.Button("Generate Randomly")
+    local_storage = gr.BrowserState(["", ""])
+
+    @btn.click(outputs=[username, password])
+    def generate_randomly():
+        u = "".join(random.choices(string.ascii_letters + string.digits, k=10))
+        p = "".join(random.choices(string.ascii_letters + string.digits, k=10))
+        return u, p
+
+    @demo.load(inputs=[local_storage], outputs=[username, password])
+    def load_from_local_storage(saved_values):
+        print("loading from local storage", saved_values)
+        return saved_values[0], saved_values[1]
+
+    @gr.on([username.change, password.change], inputs=[username, password], outputs=[local_storage])
+    def save_to_local_storage(username, password):
+        return [username, password]
+
+demo.launch()
diff --git a/gradio/__init__.py b/gradio/__init__.py
index e4087f575f..2bdbfeb21a 100644
--- a/gradio/__init__.py
+++ b/gradio/__init__.py
@@ -14,6 +14,7 @@
     Annotatedimage,
     Audio,
     BarPlot,
+    BrowserState,
     Button,
     Chatbot,
     ChatMessage,
diff --git a/gradio/components/__init__.py b/gradio/components/__init__.py
index 4c2b2c7ba8..5e35566d38 100644
--- a/gradio/components/__init__.py
+++ b/gradio/components/__init__.py
@@ -9,6 +9,7 @@
     component,
     get_component_instance,
 )
+from gradio.components.browser_state import BrowserState
 from gradio.components.button import Button
 from gradio.components.chatbot import Chatbot, ChatMessage, MessageDict
 from gradio.components.checkbox import Checkbox
@@ -88,6 +89,7 @@
     "Json",
     "Label",
     "LinePlot",
+    "BrowserState",
     "LoginButton",
     "Markdown",
     "MessageDict",
diff --git a/gradio/components/browser_state.py b/gradio/components/browser_state.py
new file mode 100644
index 0000000000..178893d9d5
--- /dev/null
+++ b/gradio/components/browser_state.py
@@ -0,0 +1,69 @@
+"""gr.BrowserState() component."""
+
+from __future__ import annotations
+
+import secrets
+import string
+from typing import Any
+
+from gradio_client.documentation import document
+
+from gradio.components.base import Component
+
+
+@document()
+class BrowserState(Component):
+    """
+    Special component that stores state in the browser's localStorage in an encrypted format.
+    """
+
+    def __init__(
+        self,
+        default_value: Any = None,
+        *,
+        storage_key: str | None = None,
+        secret: str | None = None,
+        render: bool = True,
+    ):
+        """
+        Parameters:
+            default_value: the default value that will be used if no value is found in localStorage. Should be a json-serializable value.
+            storage_key: the key to use in localStorage. If None, a random key will be generated.
+            secret: the secret key to use for encryption. If None, a random key will be generated (recommended).
+            render: should always be True, is included for consistency with other components.
+        """
+        self.default_value = default_value
+        self.secret = secret or "".join(
+            secrets.choice(string.ascii_letters + string.digits) for _ in range(16)
+        )
+        self.storage_key = storage_key or "".join(
+            secrets.choice(string.ascii_letters + string.digits) for _ in range(16)
+        )
+        super().__init__(render=render)
+
+    def preprocess(self, payload: Any) -> Any:
+        """
+        Parameters:
+            payload: Value from local storage
+        Returns:
+            Passes value through unchanged
+        """
+        return payload
+
+    def postprocess(self, value: Any) -> Any:
+        """
+        Parameters:
+            value: Value to store in local storage
+        Returns:
+            Passes value through unchanged
+        """
+        return value
+
+    def api_info(self) -> dict[str, Any]:
+        return {"type": {}, "description": "any json-serializable value"}
+
+    def example_payload(self) -> Any:
+        return "test"
+
+    def example_value(self) -> Any:
+        return "test"
diff --git a/gradio/components/state.py b/gradio/components/state.py
index 61a8fcddb9..204016096d 100644
--- a/gradio/components/state.py
+++ b/gradio/components/state.py
@@ -36,7 +36,7 @@ def __init__(
         """
         Parameters:
             value: the initial value (of arbitrary type) of the state. The provided argument is deepcopied. If a callable is provided, the function will be called whenever the app loads to set the initial value of the state.
-            render: has no effect, but is included for consistency with other components.
+            render: should always be True, is included for consistency with other components.
             time_to_live: The number of seconds the state should be stored for after it is created or updated. If None, the state will be stored indefinitely. Gradio automatically deletes state variables after a user closes the browser tab or refreshes the page, so this is useful for clearing state for potentially long running sessions.
             delete_callback: A function that is called when the state is deleted. The function should take the state value as an argument.
         """
diff --git a/guides/03_building-with-blocks/03_state-in-blocks.md b/guides/03_building-with-blocks/03_state-in-blocks.md
index 6b56b7041e..b24c4ff0b3 100644
--- a/guides/03_building-with-blocks/03_state-in-blocks.md
+++ b/guides/03_building-with-blocks/03_state-in-blocks.md
@@ -31,3 +31,15 @@ The `.change` listener for a state variable triggers after any event listener ch
 The value of a session State variable is cleared when the user refreshes the page. The value is stored on in the app backend for 60 minutes after the user closes the tab (this can be configured by the `delete_cache` parameter in `gr.Blocks`).
 
 Learn more about `State` in the [docs](https://gradio.app/docs/gradio/state).
+
+## Local State
+
+Gradio also supports **local state**, where data persists in the browser's localStorage even after the page is refreshed or closed. This is useful for storing user preferences, settings, API keys, or other data that should persist across sessions. To use local state:
+
+1. Create a `gr.BrowserState()` object. You can optionally provide an initial default value and a key to identify the data in the browser's localStorage.
+2. Use it like a regular `gr.State` component in event listeners as inputs and outputs.
+
+Here's a simple example that saves a user's username and password across sessions:
+
+$code_browserstate
+
diff --git a/js/app/src/routes/[...catchall]/+page.svelte b/js/app/src/routes/[...catchall]/+page.svelte
index 1906be7eca..8c2062a242 100644
--- a/js/app/src/routes/[...catchall]/+page.svelte
+++ b/js/app/src/routes/[...catchall]/+page.svelte
@@ -30,6 +30,7 @@
 		stylesheets?: string[];
 		path: string;
 		app_id?: string;
+		browser_state_secret?: string;
 		fill_height?: boolean;
 		fill_width?: boolean;
 		theme_hash?: number;
diff --git a/js/browserstate/Index.svelte b/js/browserstate/Index.svelte
new file mode 100644
index 0000000000..caba0c450d
--- /dev/null
+++ b/js/browserstate/Index.svelte
@@ -0,0 +1,51 @@
+<svelte:options accessors={true} />
+
+<script lang="ts">
+	import { beforeUpdate } from "svelte";
+	import { encrypt, decrypt } from "./crypto";
+	import { dequal } from "dequal/lite";
+
+	export let storage_key: string;
+	export let secret: string;
+	export let default_value: any;
+	export let value = default_value;
+	let initialized = false;
+	let old_value = value;
+
+	function load_value(): void {
+		const stored = localStorage.getItem(storage_key);
+		if (!stored) {
+			old_value = default_value;
+			value = old_value;
+			return;
+		}
+		try {
+			const decrypted = decrypt(stored, secret);
+			old_value = JSON.parse(decrypted);
+			value = old_value;
+		} catch (e) {
+			console.error("Error reading from localStorage:", e);
+			old_value = default_value;
+			value = old_value;
+		}
+	}
+
+	function save_value(): void {
+		try {
+			const encrypted = encrypt(JSON.stringify(value), secret);
+			localStorage.setItem(storage_key, encrypted);
+			old_value = value;
+		} catch (e) {
+			console.error("Error writing to localStorage:", e);
+		}
+	}
+
+	$: value && !dequal(value, old_value) && save_value();
+
+	beforeUpdate(() => {
+		if (!initialized) {
+			initialized = true;
+			load_value();
+		}
+	});
+</script>
diff --git a/js/browserstate/crypto.ts b/js/browserstate/crypto.ts
new file mode 100644
index 0000000000..108c8461bd
--- /dev/null
+++ b/js/browserstate/crypto.ts
@@ -0,0 +1,28 @@
+import CryptoJS from "crypto-js";
+
+export function encrypt(data: string, key: string): string {
+	const hashedKey = CryptoJS.SHA256(key).toString();
+	const iv = CryptoJS.lib.WordArray.random(16);
+	const encrypted = CryptoJS.AES.encrypt(data, hashedKey, {
+		iv: iv,
+		mode: CryptoJS.mode.CBC,
+		padding: CryptoJS.pad.Pkcs7
+	});
+
+	const ivString = CryptoJS.enc.Base64.stringify(iv);
+	const cipherString = encrypted.toString();
+	return ivString + ":" + cipherString;
+}
+
+export function decrypt(encryptedData: string, key: string): string {
+	const hashedKey = CryptoJS.SHA256(key).toString();
+	const [ivString, cipherString] = encryptedData.split(":");
+	const iv = CryptoJS.enc.Base64.parse(ivString);
+	const decrypted = CryptoJS.AES.decrypt(cipherString, hashedKey, {
+		iv: iv,
+		mode: CryptoJS.mode.CBC,
+		padding: CryptoJS.pad.Pkcs7
+	});
+
+	return decrypted.toString(CryptoJS.enc.Utf8);
+}
diff --git a/js/browserstate/package.json b/js/browserstate/package.json
new file mode 100644
index 0000000000..80ccbc6487
--- /dev/null
+++ b/js/browserstate/package.json
@@ -0,0 +1,33 @@
+{
+	"name": "@gradio/browserstate",
+	"version": "0.1.2",
+	"description": "Gradio UI packages",
+	"type": "module",
+	"author": "",
+	"license": "ISC",
+	"private": false,
+	"main_changeset": true,
+	"exports": {
+		".": {
+			"gradio": "./Index.svelte",
+			"svelte": "./dist/Index.svelte",
+			"types": "./dist/Index.svelte.d.ts"
+		},
+		"./package.json": "./package.json"
+	},
+	"dependencies": {
+		"dequal": "^2.0.2",
+		"crypto-js": "^4.1.1"
+	},
+	"peerDependencies": {
+		"svelte": "^4.0.0"
+	},
+	"repository": {
+		"type": "git",
+		"url": "git+https://github.com/gradio-app/gradio.git",
+		"directory": "js/state"
+	},
+	"devDependencies": {
+		"@types/crypto-js": "^4.1.1"
+	}
+}
diff --git a/js/component-test/src/routes/[...all]/+page.ts b/js/component-test/src/routes/[...all]/+page.ts
index f17a6034be..19484b62df 100644
--- a/js/component-test/src/routes/[...all]/+page.ts
+++ b/js/component-test/src/routes/[...all]/+page.ts
@@ -24,6 +24,7 @@ const comps = {
 	imageeditor: () => import("@gradio/imageeditor"),
 	json: () => import("@gradio/json"),
 	label: () => import("@gradio/label"),
+	browserstate: () => import("@gradio/browserstate"),
 	markdown: () => import("@gradio/markdown"),
 	model3d: () => import("@gradio/model3d"),
 	multimodaltextbox: () => import("@gradio/multimodaltextbox"),
diff --git a/js/core/package.json b/js/core/package.json
index b3e4127cb3..3e3e463904 100644
--- a/js/core/package.json
+++ b/js/core/package.json
@@ -34,6 +34,7 @@
 		"@gradio/imageeditor": "workspace:^",
 		"@gradio/json": "workspace:^",
 		"@gradio/label": "workspace:^",
+		"@gradio/browserstate": "workspace:^",
 		"@gradio/markdown": "workspace:^",
 		"@gradio/model3d": "workspace:^",
 		"@gradio/multimodaltextbox": "workspace:^",
diff --git a/js/spa/src/Index.svelte b/js/spa/src/Index.svelte
index 1414068b8b..2a60c85ed5 100644
--- a/js/spa/src/Index.svelte
+++ b/js/spa/src/Index.svelte
@@ -29,6 +29,7 @@
 		stylesheets?: string[];
 		path: string;
 		app_id?: string;
+		browser_state_secret?: string;
 		fill_height?: boolean;
 		fill_width?: boolean;
 		theme_hash?: number;
diff --git a/package.json b/package.json
index ab2e7b7332..517eaffa51 100644
--- a/package.json
+++ b/package.json
@@ -124,6 +124,7 @@
 		"@gradio/imageeditor": "workspace:^",
 		"@gradio/json": "workspace:^",
 		"@gradio/label": "workspace:^",
+		"@gradio/browserstate": "workspace:^",
 		"@gradio/markdown": "workspace:^",
 		"@gradio/model3d": "workspace:^",
 		"@gradio/multimodaltextbox": "workspace:^",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d40f1a1481..da308be259 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -209,6 +209,9 @@ importers:
       '@gradio/box':
         specifier: workspace:^
         version: link:js/box
+      '@gradio/browserstate':
+        specifier: workspace:^
+        version: link:js/browserstate
       '@gradio/button':
         specifier: workspace:^
         version: link:js/button
@@ -705,6 +708,22 @@ importers:
         specifier: ^4.0.0
         version: 4.2.15
 
+  js/browserstate:
+    dependencies:
+      crypto-js:
+        specifier: ^4.1.1
+        version: 4.2.0
+      dequal:
+        specifier: ^2.0.2
+        version: 2.0.3
+      svelte:
+        specifier: ^4.0.0
+        version: 4.2.15
+    devDependencies:
+      '@types/crypto-js':
+        specifier: ^4.1.1
+        version: 4.2.2
+
   js/build:
     dependencies:
       '@gradio/theme':
@@ -1010,6 +1029,9 @@ importers:
       '@gradio/box':
         specifier: workspace:^
         version: link:../box
+      '@gradio/browserstate':
+        specifier: workspace:^
+        version: link:../browserstate
       '@gradio/button':
         specifier: workspace:^
         version: link:../button
@@ -4385,6 +4407,9 @@ packages:
   '@types/cookie@0.6.0':
     resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
 
+  '@types/crypto-js@4.2.2':
+    resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
+
   '@types/css-font-loading-module@0.0.7':
     resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
 
@@ -5211,6 +5236,9 @@ packages:
     resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
     engines: {node: '>= 8'}
 
+  crypto-js@4.2.0:
+    resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
+
   css-declaration-sorter@7.2.0:
     resolution: {integrity: sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==}
     engines: {node: ^14 || ^16 || >=18}
@@ -10110,13 +10138,13 @@ snapshots:
   '@jridgewell/gen-mapping@0.3.3':
     dependencies:
       '@jridgewell/set-array': 1.1.2
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
       '@jridgewell/trace-mapping': 0.3.25
 
   '@jridgewell/gen-mapping@0.3.5':
     dependencies:
       '@jridgewell/set-array': 1.2.1
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
       '@jridgewell/trace-mapping': 0.3.25
 
   '@jridgewell/resolve-uri@3.1.2': {}
@@ -10132,12 +10160,12 @@ snapshots:
   '@jridgewell/trace-mapping@0.3.25':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
 
   '@jridgewell/trace-mapping@0.3.9':
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
 
   '@lezer/common@1.0.2': {}
 
@@ -11194,6 +11222,8 @@ snapshots:
 
   '@types/cookie@0.6.0': {}
 
+  '@types/crypto-js@4.2.2': {}
+
   '@types/css-font-loading-module@0.0.7': {}
 
   '@types/d3-dsv@3.0.0': {}
@@ -12083,6 +12113,8 @@ snapshots:
       shebang-command: 2.0.0
       which: 2.0.2
 
+  crypto-js@4.2.0: {}
+
   css-declaration-sorter@7.2.0(postcss@8.4.38):
     dependencies:
       postcss: 8.4.38
@@ -14037,7 +14069,7 @@ snapshots:
 
   magic-string@0.27.0:
     dependencies:
-      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/sourcemap-codec': 1.5.0
 
   magic-string@0.30.10:
     dependencies:
diff --git a/test/test_gradio_component_cli.py b/test/test_gradio_component_cli.py
index 7a6e02b996..7406058bca 100644
--- a/test/test_gradio_component_cli.py
+++ b/test/test_gradio_component_cli.py
@@ -15,7 +15,7 @@
     c.__name__
     for c in core_gradio_components()
     if not getattr(c, "is_template", False)
-    and c.__name__ not in ["Tab", "Form", "FormComponent"]
+    and c.__name__ not in ["Tab", "Form", "FormComponent", "BrowserState"]
 ]
 
 


Code review comments:
diff_hunk: @@ -30,6 +30,7 @@
 		stylesheets?: string[];
 		path: string;
 		app_id?: string;
+		browser_state_secret?: string;
path: js/app/src/routes/[...catchall]/+page.svelte
text: @user1:
is this still used?

@author:
Nope I’ll get rid of it