From cbef16693c459d2ebbb746d69f611fd426c2e04f Mon Sep 17 00:00:00 2001 From: June Kim Date: Sat, 9 May 2026 19:46:26 -0700 Subject: [PATCH 1/3] fix: update query_params on browser back/forward navigation Fixes #4153. When users navigate using browser back/forward buttons, query_params now updates and dependent cells re-execute. Changes: - Add UpdateQueryParamsCommand to sync URL state with kernel - Add popstate listener to detect browser navigation - Add /api/kernel/update_query_params endpoint - Update QueryParams state triggers cell re-execution via State system The fix leverages marimo's existing State system: when the browser URL changes via popstate, the frontend sends the new params to the kernel, which updates the QueryParams state object, triggering re-execution of cells that depend on query_params. --- frontend/src/core/islands/bridge.ts | 10 +++++++ .../src/core/kernel/queryParamHandlers.ts | 21 +++++++++++++ frontend/src/core/network/requests-network.ts | 8 +++++ frontend/src/core/network/requests-static.ts | 4 +++ frontend/src/core/network/types.ts | 2 ++ frontend/src/core/wasm/bridge.ts | 10 +++++++ .../websocket/useMarimoKernelConnection.tsx | 19 ++++++++++-- marimo/_runtime/commands.py | 14 +++++++++ marimo/_runtime/runtime.py | 15 ++++++++++ marimo/_server/api/endpoints/execution.py | 30 +++++++++++++++++++ marimo/_server/models/models.py | 9 ++++++ 11 files changed, 140 insertions(+), 2 deletions(-) diff --git a/frontend/src/core/islands/bridge.ts b/frontend/src/core/islands/bridge.ts index 0696d08c4ee..4411437be27 100644 --- a/frontend/src/core/islands/bridge.ts +++ b/frontend/src/core/islands/bridge.ts @@ -175,6 +175,16 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests { return null; }; + sendUpdateQueryParams: RunRequests["sendUpdateQueryParams"] = async ( + request, + ): Promise => { + await this.putControlRequest({ + type: "update-query-params", + ...request, + }); + return null; + }; + sendFunctionRequest: RunRequests["sendFunctionRequest"] = async ( request, ): Promise => { diff --git a/frontend/src/core/kernel/queryParamHandlers.ts b/frontend/src/core/kernel/queryParamHandlers.ts index e076db6863c..3ff057b6fce 100644 --- a/frontend/src/core/kernel/queryParamHandlers.ts +++ b/frontend/src/core/kernel/queryParamHandlers.ts @@ -37,3 +37,24 @@ export const queryParamHandlers = { return; }, }; + +/** + * Parse URL query parameters into the format expected by the kernel. + */ +export function parseQueryParams(): Record { + const url = new URL(window.location.href); + const params: Record = {}; + + for (const [key, value] of url.searchParams.entries()) { + const existing = params[key]; + if (existing === undefined) { + params[key] = value; + } else if (Array.isArray(existing)) { + existing.push(value); + } else { + params[key] = [existing, value]; + } + } + + return params; +} diff --git a/frontend/src/core/network/requests-network.ts b/frontend/src/core/network/requests-network.ts index e53ae139d91..1996a557cb8 100644 --- a/frontend/src/core/network/requests-network.ts +++ b/frontend/src/core/network/requests-network.ts @@ -42,6 +42,14 @@ export function createNetworkRequests(): EditRequests & RunRequests { }) .then(handleResponseReturnNull); }, + sendUpdateQueryParams: (request) => { + return getClient() + .POST("/api/kernel/update_query_params", { + body: request, + params: getParams(), + }) + .then(handleResponseReturnNull); + }, sendRestart: () => { return getClient() .POST("/api/kernel/restart_session", { diff --git a/frontend/src/core/network/requests-static.ts b/frontend/src/core/network/requests-static.ts index 285d117b65a..77a0c3558d2 100644 --- a/frontend/src/core/network/requests-static.ts +++ b/frontend/src/core/network/requests-static.ts @@ -26,6 +26,10 @@ export function createStaticRequests(): EditRequests & RunRequests { Logger.log("Viewing as static notebook"); return null; }, + sendUpdateQueryParams: async () => { + Logger.log("Query params not updated in static mode"); + return null; + }, sendFunctionRequest: async () => { toast({ title: "Static notebook", diff --git a/frontend/src/core/network/types.ts b/frontend/src/core/network/types.ts index 2a5b6c1e476..5885f16ad95 100644 --- a/frontend/src/core/network/types.ts +++ b/frontend/src/core/network/types.ts @@ -93,6 +93,7 @@ export type StdinRequest = schemas["StdinRequest"]; export type SuccessResponse = schemas["SuccessResponse"]; export type UpdateUIElementValuesRequest = schemas["UpdateUIElementValuesRequest"]; +export type UpdateQueryParamsRequest = schemas["UpdateQueryParamsRequest"]; export type UsageResponse = paths["/api/usage"]["get"]["responses"]["200"]["content"]["application/json"]; export type WorkspaceFilesRequest = schemas["WorkspaceFilesRequest"]; @@ -120,6 +121,7 @@ export interface RunRequests { sendModelValue: (request: ModelRequest) => Promise; sendInstantiate: (request: InstantiateNotebookRequest) => Promise; sendFunctionRequest: (request: InvokeFunctionRequest) => Promise; + sendUpdateQueryParams: (request: UpdateQueryParamsRequest) => Promise; } /** diff --git a/frontend/src/core/wasm/bridge.ts b/frontend/src/core/wasm/bridge.ts index 8ae6875f140..d9344477e89 100644 --- a/frontend/src/core/wasm/bridge.ts +++ b/frontend/src/core/wasm/bridge.ts @@ -416,6 +416,16 @@ export class PyodideBridge implements RunRequests, EditRequests { return null; }; + sendUpdateQueryParams: RunRequests["sendUpdateQueryParams"] = async ( + request, + ) => { + await this.putControlRequest({ + type: "update-query-params", + ...request, + }); + return null; + }; + sendFunctionRequest: RunRequests["sendFunctionRequest"] = async (request) => { await this.putControlRequest({ type: "invoke-function", diff --git a/frontend/src/core/websocket/useMarimoKernelConnection.tsx b/frontend/src/core/websocket/useMarimoKernelConnection.tsx index 89c261a705a..a2add0da3f6 100644 --- a/frontend/src/core/websocket/useMarimoKernelConnection.tsx +++ b/frontend/src/core/websocket/useMarimoKernelConnection.tsx @@ -1,7 +1,7 @@ /* Copyright 2026 Marimo. All rights reserved. */ import { useAtom, useSetAtom } from "jotai"; -import { useRef } from "react"; +import { useEffect, useRef } from "react"; import { useErrorBoundary } from "react-error-boundary"; import { toast } from "@/components/ui/use-toast"; import { getNotebook, useCellActions } from "@/core/cells/cells"; @@ -51,7 +51,7 @@ import { handleKernelReady, handleRemoveUIElements, } from "../kernel/handlers"; -import { queryParamHandlers } from "../kernel/queryParamHandlers"; +import { queryParamHandlers, parseQueryParams } from "../kernel/queryParamHandlers"; import type { SessionId } from "../kernel/session"; import { kernelStateAtom } from "../kernel/state"; import { type LayoutState, useLayoutActions } from "../layout/layout"; @@ -468,5 +468,20 @@ export function useMarimoKernelConnection(opts: { }, }); + // Listen for browser back/forward navigation to update query params + useEffect(() => { + const handlePopState = () => { + const queryParams = parseQueryParams(); + // Import at runtime to avoid circular dependency + import("../network/requests").then(({ getRequestClient }) => { + const client = getRequestClient(); + void client.sendUpdateQueryParams({ queryParams }); + }); + }; + + window.addEventListener("popstate", handlePopState); + return () => window.removeEventListener("popstate", handlePopState); + }, []); + return { connection }; } diff --git a/marimo/_runtime/commands.py b/marimo/_runtime/commands.py index 556a7e94113..6908a923621 100644 --- a/marimo/_runtime/commands.py +++ b/marimo/_runtime/commands.py @@ -449,6 +449,19 @@ def ids_and_values(self) -> list[tuple[UIElementId, Any]]: return list(zip(self.object_ids, self.values, strict=False)) +class UpdateQueryParamsCommand(Command): + """Update query parameters from browser navigation. + + Triggered when user navigates using browser back/forward buttons. + Updates query params state and re-executes dependent cells. + + Attributes: + query_params: New query parameter values from URL. + """ + + query_params: SerializedQueryParams + + class InvokeFunctionCommand(Command): """Invoke a function from a UI element. @@ -896,6 +909,7 @@ class GetCacheInfoCommand(Command): | InstallPackagesCommand # UI element and widget model operations | UpdateUIElementCommand + | UpdateQueryParamsCommand | ModelCommand | InvokeFunctionCommand # User/configuration operations diff --git a/marimo/_runtime/runtime.py b/marimo/_runtime/runtime.py index 163c3e47192..6a17d140d39 100644 --- a/marimo/_runtime/runtime.py +++ b/marimo/_runtime/runtime.py @@ -150,6 +150,7 @@ StorageListEntriesCommand, SyncGraphCommand, UpdateCellConfigCommand, + UpdateQueryParamsCommand, UpdateUIElementCommand, UpdateUserConfigCommand, ValidateSQLCommand, @@ -2432,6 +2433,19 @@ async def handle_function_call(request: InvokeFunctionCommand) -> None: ), ) + async def handle_update_query_params( + request: UpdateQueryParamsCommand, + ) -> None: + # Update the kernel's query_params state + self.query_params._params.clear() + self.query_params._params.update(request.query_params) + # Trigger state update to re-run dependent cells + self.query_params._set_value(self.query_params._params) + # Process the state updates + if self.state_updates: + await self._run_cells(set()) + broadcast_notification(CompletedRunNotification()) + async def handle_set_user_config( request: UpdateUserConfigCommand, ) -> None: @@ -2461,6 +2475,7 @@ async def handle_stop(request: StopKernelCommand) -> None: handler.register(RenameNotebookCommand, handle_rename) handler.register(UpdateCellConfigCommand, self.set_cell_config) handler.register(UpdateUIElementCommand, handle_set_ui_element_value) + handler.register(UpdateQueryParamsCommand, handle_update_query_params) handler.register(ModelCommand, handle_receive_model_message) handler.register(UpdateUserConfigCommand, handle_set_user_config) handler.register(StopKernelCommand, handle_stop) diff --git a/marimo/_server/api/endpoints/execution.py b/marimo/_server/api/endpoints/execution.py index 1b94f81fa32..99bcb9e263c 100644 --- a/marimo/_server/api/endpoints/execution.py +++ b/marimo/_server/api/endpoints/execution.py @@ -115,6 +115,36 @@ async def set_model_values( return await dispatch_control_request(request, ModelRequest) +@router.post("/update_query_params") +@requires("read") +async def update_query_params( + *, + request: Request, +) -> BaseResponse: + """ + parameters: + - in: header + name: Marimo-Session-Id + schema: + type: string + required: true + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateQueryParamsRequest" + responses: + 200: + description: Update query parameters from browser navigation + content: + application/json: + schema: + $ref: "#/components/schemas/SuccessResponse" + """ + from marimo._server.models.models import UpdateQueryParamsRequest + return await dispatch_control_request(request, UpdateQueryParamsRequest) + + @router.post("/instantiate") @requires("edit") async def instantiate( diff --git a/marimo/_server/models/models.py b/marimo/_server/models/models.py index c21a652adac..53d6ea86b03 100644 --- a/marimo/_server/models/models.py +++ b/marimo/_server/models/models.py @@ -25,9 +25,11 @@ ModelCommand, PreviewDatasetColumnCommand, PreviewSQLTableCommand, + SerializedQueryParams, StorageDownloadCommand, StorageListEntriesCommand, UpdateCellConfigCommand, + UpdateQueryParamsCommand, UpdateUIElementCommand, UpdateUserConfigCommand, ValidateSQLCommand, @@ -80,6 +82,13 @@ def as_command(self) -> UpdateUIElementCommand: ) +class UpdateQueryParamsRequest(UpdateQueryParamsCommand, tag=False): + def as_command(self) -> UpdateQueryParamsCommand: + return UpdateQueryParamsCommand( + query_params=self.query_params, + ) + + class ModelRequest(ModelCommand, tag=False): def as_command(self) -> ModelCommand: return ModelCommand( From 6b0bed036ec8739ed87e9d57961424af1789b86b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 02:54:09 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- frontend/src/core/websocket/useMarimoKernelConnection.tsx | 5 ++++- marimo/_server/api/endpoints/execution.py | 1 + marimo/_server/models/models.py | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/core/websocket/useMarimoKernelConnection.tsx b/frontend/src/core/websocket/useMarimoKernelConnection.tsx index a2add0da3f6..e3280cbc123 100644 --- a/frontend/src/core/websocket/useMarimoKernelConnection.tsx +++ b/frontend/src/core/websocket/useMarimoKernelConnection.tsx @@ -51,7 +51,10 @@ import { handleKernelReady, handleRemoveUIElements, } from "../kernel/handlers"; -import { queryParamHandlers, parseQueryParams } from "../kernel/queryParamHandlers"; +import { + queryParamHandlers, + parseQueryParams, +} from "../kernel/queryParamHandlers"; import type { SessionId } from "../kernel/session"; import { kernelStateAtom } from "../kernel/state"; import { type LayoutState, useLayoutActions } from "../layout/layout"; diff --git a/marimo/_server/api/endpoints/execution.py b/marimo/_server/api/endpoints/execution.py index 99bcb9e263c..35fb32fce59 100644 --- a/marimo/_server/api/endpoints/execution.py +++ b/marimo/_server/api/endpoints/execution.py @@ -142,6 +142,7 @@ async def update_query_params( $ref: "#/components/schemas/SuccessResponse" """ from marimo._server.models.models import UpdateQueryParamsRequest + return await dispatch_control_request(request, UpdateQueryParamsRequest) diff --git a/marimo/_server/models/models.py b/marimo/_server/models/models.py index 53d6ea86b03..3567162b9da 100644 --- a/marimo/_server/models/models.py +++ b/marimo/_server/models/models.py @@ -25,7 +25,6 @@ ModelCommand, PreviewDatasetColumnCommand, PreviewSQLTableCommand, - SerializedQueryParams, StorageDownloadCommand, StorageListEntriesCommand, UpdateCellConfigCommand, From 81d54da7bed5b2eef747e40bbfc9a2b1942c256b Mon Sep 17 00:00:00 2001 From: June Kim Date: Mon, 11 May 2026 20:08:58 -0700 Subject: [PATCH 3/3] fix: register UpdateQueryParamsRequest in OpenAPI schema and frontend wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new update_query_params endpoint was missing from the OpenAPI schema generation (MODELS list) and the generated TypeScript types. Also missing from the frontend mock, lazy-request proxy, and toasting wrapper—causing TypeScript build failures. --- frontend/src/__mocks__/requests.ts | 1 + frontend/src/core/network/requests-lazy.ts | 1 + .../src/core/network/requests-toasting.tsx | 1 + marimo/_cli/development/commands.py | 2 + marimo/_server/api/endpoints/execution.py | 3 +- packages/openapi/api.yaml | 58 +++++++++++++++++ packages/openapi/src/api.ts | 65 +++++++++++++++++++ 7 files changed, 129 insertions(+), 2 deletions(-) diff --git a/frontend/src/__mocks__/requests.ts b/frontend/src/__mocks__/requests.ts index 87596fce8f1..098fa9d4f57 100644 --- a/frontend/src/__mocks__/requests.ts +++ b/frontend/src/__mocks__/requests.ts @@ -33,6 +33,7 @@ export const MockRequestClient = { saveAppConfig: vi.fn().mockResolvedValue({}), saveCellConfig: vi.fn().mockResolvedValue({}), sendFunctionRequest: vi.fn().mockResolvedValue({}), + sendUpdateQueryParams: vi.fn().mockResolvedValue(null), sendInstallMissingPackages: vi.fn().mockResolvedValue({}), readCode: vi.fn().mockResolvedValue({ contents: "" }), readSnippets: vi.fn().mockResolvedValue({ snippets: [] }), diff --git a/frontend/src/core/network/requests-lazy.ts b/frontend/src/core/network/requests-lazy.ts index 9840cb86ff8..5e54ac99c7e 100644 --- a/frontend/src/core/network/requests-lazy.ts +++ b/frontend/src/core/network/requests-lazy.ts @@ -63,6 +63,7 @@ const ACTIONS: Record = { sendSave: "waitForConnectionOpen", invokeAiTool: "waitForConnectionOpen", sendFunctionRequest: "waitForConnectionOpen", + sendUpdateQueryParams: "waitForConnectionOpen", // Session-based operations that wait for connection sendRename: "waitForConnectionOpen", diff --git a/frontend/src/core/network/requests-toasting.tsx b/frontend/src/core/network/requests-toasting.tsx index f91e960df19..5a34e197a95 100644 --- a/frontend/src/core/network/requests-toasting.tsx +++ b/frontend/src/core/network/requests-toasting.tsx @@ -20,6 +20,7 @@ export function createErrorToastingRequests( sendModelValue: "Failed to update model value", sendInstantiate: "Failed to instantiate", sendFunctionRequest: "Failed to send function request", + sendUpdateQueryParams: "Failed to update query params", sendRestart: "Failed to restart", sendDocumentTransaction: "Failed to sync document transaction", sendRun: "Failed to run", diff --git a/marimo/_cli/development/commands.py b/marimo/_cli/development/commands.py index 44e56838bad..c0719ab5806 100644 --- a/marimo/_cli/development/commands.py +++ b/marimo/_cli/development/commands.py @@ -341,6 +341,7 @@ def _generate_server_api_schema() -> dict[str, Any]: snippets.Snippet, snippets.Snippets, commands.UpdateUIElementCommand, + commands.UpdateQueryParamsCommand, # Requests/responses completion.VariableContext, completion.SchemaColumn, @@ -453,6 +454,7 @@ def _generate_server_api_schema() -> dict[str, Any]: models.NotebookDocumentTransactionRequest, models.FocusCellRequest, models.UpdateUIElementValuesRequest, + models.UpdateQueryParamsRequest, models.UpdateUIElementRequest, models.UpdateUserConfigRequest, models.ModelRequest, diff --git a/marimo/_server/api/endpoints/execution.py b/marimo/_server/api/endpoints/execution.py index 35fb32fce59..5777ddb7937 100644 --- a/marimo/_server/api/endpoints/execution.py +++ b/marimo/_server/api/endpoints/execution.py @@ -27,6 +27,7 @@ InvokeFunctionRequest, ModelRequest, SuccessResponse, + UpdateQueryParamsRequest, UpdateUIElementValuesRequest, ) from marimo._server.router import APIRouter @@ -141,8 +142,6 @@ async def update_query_params( schema: $ref: "#/components/schemas/SuccessResponse" """ - from marimo._server.models.models import UpdateQueryParamsRequest - return await dispatch_control_request(request, UpdateQueryParamsRequest) diff --git a/packages/openapi/api.yaml b/packages/openapi/api.yaml index 6e37b6f33d6..1f27f47d541 100644 --- a/packages/openapi/api.yaml +++ b/packages/openapi/api.yaml @@ -2483,6 +2483,7 @@ components: - $ref: '#/components/schemas/UpdateCellConfigCommand' - $ref: '#/components/schemas/InstallPackagesCommand' - $ref: '#/components/schemas/UpdateUIElementCommand' + - $ref: '#/components/schemas/UpdateQueryParamsCommand' - $ref: '#/components/schemas/ModelCommand' - $ref: '#/components/schemas/InvokeFunctionCommand' - $ref: '#/components/schemas/UpdateUserConfigCommand' @@ -2526,6 +2527,7 @@ components: storage-list-entries: '#/components/schemas/StorageListEntriesCommand' sync-graph: '#/components/schemas/SyncGraphCommand' update-cell-config: '#/components/schemas/UpdateCellConfigCommand' + update-query-params: '#/components/schemas/UpdateQueryParamsCommand' update-ui-element: '#/components/schemas/UpdateUIElementCommand' update-user-config: '#/components/schemas/UpdateUserConfigCommand' validate-sql: '#/components/schemas/ValidateSQLCommand' @@ -5191,6 +5193,42 @@ components: - cellIdsToOutput title: UpdateCellOutputsRequest type: object + UpdateQueryParamsCommand: + description: "Update query parameters from browser navigation.\n\n Triggered\ + \ when user navigates using browser back/forward buttons.\n Updates query\ + \ params state and re-executes dependent cells.\n\n Attributes:\n \ + \ query_params: New query parameter values from URL." + properties: + queryParams: + additionalProperties: + anyOf: + - type: string + - items: + type: string + type: array + type: object + type: + enum: + - update-query-params + required: + - type + - queryParams + title: UpdateQueryParamsCommand + type: object + UpdateQueryParamsRequest: + properties: + queryParams: + additionalProperties: + anyOf: + - type: string + - items: + type: string + type: array + type: object + required: + - queryParams + title: UpdateQueryParamsRequest + type: object UpdateUIElementCommand: description: "Update UI element values.\n\n Triggered when users interact\ \ with UI elements (sliders, inputs, dropdowns, etc.).\n Updates element\ @@ -6701,6 +6739,26 @@ paths: type: string type: object description: Successfully closed existing sessions + /api/kernel/update_query_params: + post: + parameters: + - in: header + name: Marimo-Session-Id + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateQueryParamsRequest' + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/SuccessResponse' + description: Update query parameters from browser navigation /api/lsp/health: get: responses: diff --git a/packages/openapi/src/api.ts b/packages/openapi/src/api.ts index 02a4cee9b6a..1a71420b7a5 100644 --- a/packages/openapi/src/api.ts +++ b/packages/openapi/src/api.ts @@ -2604,6 +2604,47 @@ export interface paths { patch?: never; trace?: never; }; + "/api/kernel/update_query_params": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header: { + "Marimo-Session-Id": string; + }; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UpdateQueryParamsRequest"]; + }; + }; + responses: { + /** @description Update query parameters from browser navigation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SuccessResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/lsp/health": { parameters: { query?: never; @@ -4867,6 +4908,7 @@ export interface components { | components["schemas"]["UpdateCellConfigCommand"] | components["schemas"]["InstallPackagesCommand"] | components["schemas"]["UpdateUIElementCommand"] + | components["schemas"]["UpdateQueryParamsCommand"] | components["schemas"]["ModelCommand"] | components["schemas"]["InvokeFunctionCommand"] | components["schemas"]["UpdateUserConfigCommand"] @@ -6532,6 +6574,29 @@ export interface components { ]; }; }; + /** + * UpdateQueryParamsCommand + * @description Update query parameters from browser navigation. + * + * Triggered when user navigates using browser back/forward buttons. + * Updates query params state and re-executes dependent cells. + * + * Attributes: + * query_params: New query parameter values from URL. + */ + UpdateQueryParamsCommand: { + queryParams: { + [key: string]: string | string[]; + }; + /** @enum {unknown} */ + type: "update-query-params"; + }; + /** UpdateQueryParamsRequest */ + UpdateQueryParamsRequest: { + queryParams: { + [key: string]: string | string[]; + }; + }; /** * UpdateUIElementCommand * @description Update UI element values.