Add browser-local run history and loading - #13718
Conversation
|
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
doesn't look like its working as expected here, |
|
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 |
🪼 branch checks and previews
Install Gradio from this PR pip install https://huggingface.co/buckets/gradio/pypi-previews/resolve/de6034c8ec1e1921b797068477c825d6020a62ab/gradio-6.23.1-py3-none-any.whlInstall 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"; |
- 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>
There was a problem hiding this comment.
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/runshistory 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 sendprocess_completedto 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.
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>
|
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 Screen.Recording.2026-08-10.at.2.15.02.PM.movShould hopefully be a good foundation for: #13638 |
…tory # Conflicts: # js/core/src/Blocks.svelte
hannahblair
left a comment
There was a problem hiding this comment.
LGTM thanks @abidlabs! This is awesome 🙌
🦄 change detectedThis Pull Request includes changes to the following packages.
|
|
Thanks for the review @hannahblair! Will merge this in |
|
Fixed a couple of issues related to:
Out of scope of this PR @hannahblair:
|
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>

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:
gr.Workflowbut I haven't tested this (cc @hannahblair)Closes: #13717