Skip to content

Add browser-local run history and loading - #13718

Merged
abidlabs merged 26 commits into
mainfrom
agent/browser-run-history
Aug 11, 2026
Merged

Add browser-local run history and loading#13718
abidlabs merged 26 commits into
mainfrom
agent/browser-run-history

Conversation

@abidlabs

@abidlabs abidlabs commented Aug 6, 2026

Copy link
Copy Markdown
Member

Adds a run history for Gradio apps at /gradio_api/runs. The shared JavaScript client records endpoint calls in browser local storage, including page, inputs, outputs, status, timestamps, and component metadata.

Screen.Recording.2026-08-06.at.3.49.48.PM.mov

The history page:

  • works for Interface, Blocks, multipage, and server-mode apps that use the shared JS client. I believe it should also work for gr.Workflow but I haven't tested this (cc @hannahblair)
  • groups saved runs by endpoint
  • uses Gradio's compact Examples component renderers for modality-aware previews
  • loads saved inputs and outputs back onto the original page without re-executing the endpoint
  • Currently, everything is saved in localstorage for privacy reasons, but we could store this data to a persistent file, or even a Bucket (on HF Spaces)

Closes: #13717

@abidlabs
abidlabs requested a review from hysts August 6, 2026 22:48
@hannahblair

hannahblair commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

nice! using js client submit method is clean. i also like the grouping by endpoint approach in the UI

i have a couple tiny UI tweaks/ideas for the panel. also, it'd be cool to have the runtime per run added too so you could easily compare model output times. going to do a bit more testing, but this looks good

im thinking as a result of this PR i can tweak #13638 to remove the workflow history panel which your ui replaces, and trim down some logic

works for Interface, Blocks, multipage, and server-mode apps that use the shared JS client. I believe it should also work for gr.Workflow but I haven't tested this (cc @hannahblair)

image

doesn't look like its working as expected here, load_initial is all i see (which i don't think we want to see) even after several runs. the reason is that workflow's server funcs don't go through submit(), they use component_server, so they wouldn;t get intercepted. would be a pretty easy fix in my workflow bucket pr

@hannahblair

Copy link
Copy Markdown
Collaborator

im wondering where we could put the connect/sync bucket button. maybe in the /runs page but we need to make that accessible within the ui of the workflow because right now you need to navigate to the url

@abidlabs

abidlabs commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

im wondering where we could put the connect/sync bucket button. maybe in the /runs page but we need to make that accessible within the ui of the workflow because right now you need to navigate to the url

agreed, this is not super visible right now

@gradio-pr-bot

gradio-pr-bot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

🪼 branch checks and previews

Name Status URL
Spaces ready! Spaces preview
Website ready! Website preview
Storybook ready! Storybook preview
🦄 Changes detected! Details

Install Gradio from this PR

pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/de6034c8ec1e1921b797068477c825d6020a62ab/gradio-6.23.1-py3-none-any.whl

Install Gradio Python Client from this PR

pip install "gradio-client @ git+https://github.com/gradio-app/gradio@de6034c8ec1e1921b797068477c825d6020a62ab#subdirectory=client/python"

Import Gradio JS Client from this PR via CDN

import { Client } from "https://huggingface.co/buckets/gradio/npm-previews/resolve/de6034c8ec1e1921b797068477c825d6020a62ab/browser.js";

abidlabs and others added 8 commits August 10, 2026 14:51
- Record how long each run took. The queue reports the server-side function
  duration on every completed run, so use that where available and fall back
  to the elapsed client time; queue wait is tracked separately so it does not
  inflate the runtime. Shown as "Completed in 1.21s", with the full breakdown
  on hover.
- Add a "History" footer link, alongside "Use via API" and "Settings", so the
  page is reachable without opening settings. It appears once the browser has
  saved a run, which the client now notifies listeners about (`storage` only
  fires in other tabs, so the link needed two runs to show up).
- Handle `gr.State`: its value lives on the server and is always saved as
  null, so skip it when previewing a run, and stop writing null back over the
  component's default when loading one. `RunValue` also falls back to text for
  any component without an example renderer instead of failing to mount.
- Show the status as coloured text with a dot rather than a badge, so it no
  longer competes with the load button next to it, and soften that button.
- Fix the run history URL: `config.root` has no trailing slash, so the
  settings link pointed at `http://host:7860gradio_api/runs`.
- Keep the stored component metadata index-aligned with the saved payloads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A generator reports `duration` per chunk, so the value on `process_completed`
covers only the final iteration. A 1.4s streaming run was showing up as
"Completed in 1ms". Track whether a run generated or streamed, and fall back
to the measured elapsed time for those.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Rename the footer link to "Runs" and move it left of "Use via API". The
  `footer_links` value is now "runs" too, matching both the label and the
  `/gradio_api/runs` route.
- Replace the history icon. The old one's arrowhead barbs both splayed the
  same way, so it read as a broken check mark rather than an arrow following
  the arc; use the standard clock-with-counterclockwise-arrow glyph, which
  also stays legible at footer size.
- Give the run history page a footer, so it is not a dead end. Its first item
  leads back to the app. This lives in a new `PageFooter` component in
  js/core rather than being duplicated, since only the links that work
  outside `Blocks` belong on these pages: the API docs and settings panels
  are not mounted there.
- Move the play glyph off the endpoint heading and onto "Load run", where it
  describes an action rather than decorating a label.
- Make the per-endpoint run count a pill so it reads at a glance.
- Show a failed run's message on hover instead of inline, with an info icon
  and a dotted underline to advertise it, plus the text for screen readers.
  The messages are long and only relevant to the one row that failed.
- Fold the run count into the page title: "Run history (3) logged in ...".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dotted underline promised a message that never arrived: a native `title`
tooltip is slow enough to miss and cannot be clicked. Replace it with a real
bubble that appears immediately on hover, toggles on click for touch, shows on
keyboard focus, and dismisses on Escape. Escape also drops focus, since
pressing a key would otherwise promote the trigger to `:focus-visible` and
keep the message on screen after dismissing it.

The info icon goes away too, as the underline already advertises the message,
so a failed run gets the same status dot as every other run. The groups no
longer clip their overflow, so a bubble cannot be cut off; the header rounds
its own corners instead.

Also fold the run count into the heading: "Run history (3)".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gradio wires up its own dependencies for loading examples, flagging and
clearing, and `gr.Examples` marks all of them "undocumented". Two of them fire
on page load when examples are preloaded, so simply opening an app with cached
examples added two runs to the history before the user touched anything, and
clicking an example added more. Record only "public" dependencies, which is
also the set a user could call as an API and so matches what "runs" means.

Run history is a side effect of submitting, never the point of it, so route
every exported function through a helper that swallows and logs failures.
Storage quota, a value that will not serialise, a browser with storage
disabled, or a listener that throws can no longer propagate into `submit`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Storage was keyed on the app's URL path, so stopping one app and starting a
different one on the same port made them share a history. Key on `app_id`
instead. That id is regenerated whenever an app starts, so a restart now
begins a fresh history — including in reload mode — which is the accepted
trade for keeping apps separate.

That does mean every restart orphans a history, so prune the ones left behind
rather than let the browser accumulate every history an app ever had until it
runs out of room for the current one. Pruning also runs before giving up on
the oldest runs of the current app when a write hits the quota.

The Example renderers are reused for previews, but they were written for the
values in an `Examples` dataset, which are not always shaped like the live
values a run produces: a dataframe arrives as `{headers, data}` where the
renderer wants rows of cells, and audio and file renderers print their value
as text, so a `FileData` came out as "[object Object]". Reshape the value for
the renderer instead of falling back to JSON, and fall back to text only when
a renderer draws nothing at all. Summaries also read a `gr.Label` as its
winning label and `gr.HighlightedText` as its sentence, and a sub-millisecond
run now reads "<1ms" rather than "0ms".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… fallback

The dataframe renderer draws a "more rows" gradient as an absolutely
positioned child. The preview cell was not a containing block, so the gradient
sized itself against the page instead: a 1280x1015 white wash over the whole
run that swallowed the clicks meant for Load run. Make the cell a containing
block so nothing a renderer positions can escape it, and hide that gradient,
which fades towards a table background the cell does not have.

Only the image, video, gallery and image-editor renderers draw a file by
reading `url` off the value; everything else prints what it is handed. Replace
the list of renderers to hand a file name with an allowlist of the ones that
render files, so a component that is not recognised — `gr.Model3D`, which was
showing "[object Object]" — degrades to its file name rather than to that.

Recording is now decided by what triggered a run rather than by API
visibility. Visibility was the wrong signal: `gr.ChatInterface` submits chat
through an "undocumented" dependency and exposes a separate "public" endpoint
for API callers, so filtering on it dropped every real chat run. Walk back up
the `.then()` chain instead and skip runs rooted at the page loading or at
Gradio filling in an example, plus bookkeeping events whose whole output is
server-side state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The run history now covers exactly the dependencies the API page lists, using
the same `api_visibility === "public"` predicate it does, so the two pages
never disagree about what an endpoint is. This also keeps out everything
Gradio wires up for itself — example loading, flagging and clear buttons are
all "undocumented" or "private", and some of them fire on page load.

The accepted trade is that a component whose UI submits through an
undocumented dependency records nothing for its in-app use. `gr.ChatInterface`
is the notable one: it submits chat through `_submit_fn` and exposes a
separate public `respond` endpoint for API callers.

This replaces the trigger-chain heuristic, which kept those in-app runs but
also kept the surrounding plumbing, so a single chat message produced six
entries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds browser-local run history, previews, and replay loading across Gradio apps.

Changes:

  • Records endpoint runs in local storage.
  • Adds the /gradio_api/runs history interface.
  • Adds navigation, replay support, routing, and tests.

Reviewed changes

Copilot reviewed 18 out of 20 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
test/test_routes.py Tests route and footer defaults.
js/spa/src/RunValue.svelte Renders saved value previews.
js/spa/src/RunHistory.svelte Implements the history page.
js/spa/src/run_value.ts Transforms and summarizes values.
js/spa/src/Index.svelte Routes history and restores runs.
js/spa/index.html Adds configurable base URL.
js/core/src/PageFooter.svelte Adds auxiliary-page footer.
js/core/src/lang/en.json Adds history labels.
js/core/src/Blocks.svelte Adds history footer navigation.
js/core/src/api_docs/Settings.svelte Adds history settings link.
js/core/src/api_docs/img/history-logo.svg Adds history icon.
js/core/src/api_docs/img/app-logo.svg Adds app icon.
js/core/package.json Exports the page footer.
gradio/server.py Extends footer-link typing.
gradio/routes.py Serves the history route.
gradio/blocks.py Enables history links by default.
client/js/src/utils/submit.ts Records submission events.
client/js/src/utils/run_history.ts Implements history persistence.
client/js/src/test/run_history.test.ts Tests storage utilities.
client/js/src/index.ts Exports history APIs and types.
Suppressed comments (1)

client/js/src/utils/submit.ts:190

  • Run history is updated only from emitted events, but cancel() does not emit a terminal event; the queue also does not send process_completed to a cancelled event. Consequently, stopping a queued/generator run leaves its saved entry permanently marked “Running.” Mark the history entry cancelled (or otherwise terminal) from the cancellation/iterator-close paths.
					"The `/reset` endpoint could not be called. Subsequent endpoint results may be unreliable."

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread client/js/src/utils/run_history.ts Outdated
Comment thread js/spa/src/RunHistory.svelte Outdated
Comment thread js/spa/src/run_value.ts Outdated
Comment thread js/spa/src/RunHistory.svelte Outdated
Comment thread js/spa/src/RunHistory.svelte
Comment thread js/spa/src/Index.svelte
abidlabs and others added 3 commits August 10, 2026 14:03
Drop the timing and hardening cases from the client tests, which had grown to
cover more than the behaviour warranted, and the two `footer_links` cases from
the route tests.

Document the run history in the API page guide, which is where it belongs now
that it records the same endpoints that page lists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Stop rewriting the whole history on every event. Events that change nothing
  worth saving no longer write at all, and a generator's per-chunk events are
  coalesced to at most one write per 500ms, so a token stream no longer
  restringifies the stored runs on the main thread for every token. The newest
  held-back chunk is carried in memory and reapplied on completion, since each
  call re-reads from storage and would otherwise drop it.
- Identify a file by the `gradio.FileData` discriminator that both the backend
  and the JS client stamp on it. Matching on a `path` or `url` property claimed
  ordinary JSON such as `{"url": "https://example.com"}` as a file and showed
  it as a file name instead of JSON.
- Round the total seconds before splitting them, so 119.6s reads "2m 0s"
  rather than "1m 60s".
- Say runs are "saved in" local storage rather than "logged in" it, which read
  as a claim about being signed in.
- Keep the per-section "Inputs" and "Outputs" headings in the accessibility
  tree on desktop. They were `display: none` while the column headings above
  them are `aria-hidden`, leaving screen reader users with no way to tell the
  two columns apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@abidlabs

abidlabs commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

The run history now shows the running time as well @hannahblair. Also made a bunch of fixes (e.g. handling endpoints that take input/output State, ensuring that apps started on the same port don't leak history, and many others). Also made it a bit more prominent showing the Run history in the footer if there's at least one run, wdyt?

Screen.Recording.2026-08-10.at.2.15.02.PM.mov

Should hopefully be a good foundation for: #13638

@abidlabs
abidlabs marked this pull request as ready for review August 10, 2026 21:17
Comment thread client/js/src/utils/run_history.ts Outdated

@hannahblair hannahblair left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM thanks @abidlabs! This is awesome 🙌

@gradio-pr-bot

Copy link
Copy Markdown
Collaborator

🦄 change detected

This Pull Request includes changes to the following packages.

Package Version
@gradio/client minor
@gradio/core minor
@self/spa minor
gradio minor

  • Add browser-local run history and loading

Something isn't right?

  • Maintainers can change the version label to modify the version bump.
  • If the bot has failed to detect any changes, or if this pull request needs to update multiple packages to different versions or requires a more comprehensive changelog entry, maintainers can update the changelog file directly.

@abidlabs

Copy link
Copy Markdown
Member Author

Thanks for the review @hannahblair! Will merge this in

@abidlabs

Copy link
Copy Markdown
Member Author

Fixed a couple of issues related to:

  • Auth (ensuring that logged in users could not see each other's PRs)
  • Added a run_history flag in .launch() if a developer wants to disable this feature

Out of scope of this PR @hannahblair:

  • Getting this to work with Workflows
  • Bucket-based persistence

Key the browser's run history on the logged-in user as well as the app,
so that signing into an authenticated app on a shared browser does not
surface the previous user's runs.

Add `run_history` to `launch()`, `mount_gradio_app()` and `gr.Server`,
along with a `GRADIO_RUN_HISTORY` env var. It defaults to True. When
False nothing is recorded, /gradio_api/runs 404s, the footer and
settings links are dropped, and anything already stored for the app is
cleared from the browser on the next load.

Recording is also gated on `record_history` in the JS client's options,
which defaults to true so that server-mode apps build a history through
the client alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@abidlabs
abidlabs merged commit a9e8382 into main Aug 11, 2026
26 of 27 checks passed
@abidlabs
abidlabs deleted the agent/browser-run-history branch August 11, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add browser-local run history and loading for Gradio apps

4 participants