Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e880977
Add browser-local run history and loading
abidlabs Aug 6, 2026
54bacd3
Refine run history controls and storage UI
abidlabs Aug 6, 2026
77d5c57
Simplify storage label and emphasize loading
abidlabs Aug 6, 2026
26b60e1
Condense run history header
abidlabs Aug 6, 2026
6acafb4
Link run history from settings
abidlabs Aug 6, 2026
a505b07
Show run count in settings
abidlabs Aug 6, 2026
10792e0
Merge branch 'main' into agent/browser-run-history
abidlabs Aug 7, 2026
35b2c17
Merge branch 'main' into agent/browser-run-history
gradio-pr-bot Aug 10, 2026
e0de375
Merge branch 'main' into agent/browser-run-history
abidlabs Aug 10, 2026
c6af1ff
Add per-run runtime, a footer link, and gr.State handling to run history
abidlabs Aug 10, 2026
6e812c7
Time streamed runs by elapsed time, not their last chunk
abidlabs Aug 10, 2026
caa04dd
Rework the run history entry points and row chrome
abidlabs Aug 10, 2026
cbce975
Reveal a failed run's message on hover instead of via `title`
abidlabs Aug 10, 2026
7602b95
Only record real API endpoints, and never let saving break an app
abidlabs Aug 10, 2026
b4ab0ce
Key run history on the app id, and reshape values for the previews
abidlabs Aug 10, 2026
930b90a
Stop the dataframe overlay covering the page, and widen the file-name…
abidlabs Aug 10, 2026
968547b
Record the same endpoints the API page documents
abidlabs Aug 10, 2026
a57714e
Trim the run history tests and document the feature
abidlabs Aug 10, 2026
8a740e5
Merge branch 'main' into agent/browser-run-history
abidlabs Aug 10, 2026
9bfa4f0
Address review feedback on the run history
abidlabs Aug 10, 2026
49c444b
Merge remote-tracking branch 'origin/main' into agent/browser-run-his…
abidlabs Aug 11, 2026
b1fd4bf
Fix pruning of staged run replays
abidlabs Aug 11, 2026
18d03ac
Merge remote-tracking branch 'origin/main' into agent/browser-run-his…
abidlabs Aug 11, 2026
b5f1f8d
Merge branch 'main' into agent/browser-run-history
abidlabs Aug 11, 2026
2276c5c
add changeset
gradio-pr-bot Aug 11, 2026
de6034c
Scope run history per user and add a run_history switch
abidlabs Aug 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions client/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ export { submit } from "./utils/submit";
export { upload_files } from "./utils/upload_files";
export { FileData, upload, prepare_files } from "./upload";
export { handle_file } from "./helpers/data";
export {
clear_run_history,
consume_run_history_replay,
delete_run_history,
on_run_history_change,
read_run_history,
run_history_url,
stage_run_history_replay,
type StoredRunComponent,
type StoredRun
} from "./utils/run_history";

export type {
SpaceStatus,
Expand Down
324 changes: 324 additions & 0 deletions client/js/src/test/run_history.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
import { afterEach, describe, expect, test, vi } from "vitest";

import {
clear_run_history,
consume_run_history_replay,
delete_run_history,
on_run_history_change,
read_run_history,
run_history_url,
stage_run_history_replay,
start_run_history,
update_run_history,
update_run_inputs
} from "../utils/run_history";

const app_id = "app-under-test";
const other_app = "some-other-app";
const replacement_apps = Array.from(
{ length: 8 },
(_, index) => `replacement-app-${index}`
);
const in_browser = typeof window !== "undefined";

afterEach(() => {
clear_run_history(app_id);
clear_run_history(other_app);
for (const replacement_app of replacement_apps) {
clear_run_history(replacement_app);
}
vi.useRealTimers();
});

describe.skipIf(!in_browser)("run history", () => {
test("stores runs per app id with the page and inputs", () => {
const id = start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["hello"],
input_components: [
{
type: "textbox",
component_class_id: "textbox-id",
props: { label: "Prompt" }
}
]
});

const runs = read_run_history(app_id);
expect(runs).toHaveLength(1);
expect(runs[0]).toMatchObject({
id,
endpoint: "/predict",
api_name: "/predict",
inputs: ["hello"],
status: "running",
input_components: [
{
type: "textbox",
component_class_id: "textbox-id",
props: { label: "Prompt" }
}
],
page: `${window.location.pathname}${window.location.search}`
});
expect(read_run_history(other_app)).toEqual([]);
});

test("replaces inputs with their upload-processed values", () => {
const id = start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: [new File(["hello"], "hello.txt")]
});

update_run_inputs(app_id, id, [
{ path: "/tmp/hello.txt", url: "/gradio_api/file=/tmp/hello.txt" }
]);

expect(read_run_history(app_id)[0].inputs).toEqual([
{ path: "/tmp/hello.txt", url: "/gradio_api/file=/tmp/hello.txt" }
]);
});

test("updates outputs and completion state", () => {
const id = start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["hello"]
});

update_run_history(app_id, id, {
type: "data",
endpoint: "/predict",
fn_index: 0,
data: ["hello hello"]
});
update_run_history(app_id, id, {
type: "status",
endpoint: "/predict",
fn_index: 0,
queue: false,
stage: "complete",
time: new Date("2025-05-16T21:45:00Z")
});

expect(read_run_history(app_id)[0]).toMatchObject({
outputs: ["hello hello"],
status: "completed",
completed_at: "2025-05-16T21:45:00.000Z"
});
});

test("records the runtime reported by the server", () => {
const id = start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["hello"]
});

update_run_history(app_id, id, {
type: "status",
endpoint: "/predict",
fn_index: 0,
queue: true,
stage: "complete",
cache_duration: 1.25,
time: new Date("2025-05-16T21:45:00Z")
});

expect(read_run_history(app_id)[0]).toMatchObject({
duration_ms: 1250,
completed_at: "2025-05-16T21:45:00.000Z"
});
});

test("records failed runs and their error message", () => {
const id = start_run_history({
app_id,
endpoint: 3,
api_name: "Function 3",
fn_index: 3,
inputs: [{ circular: null }]
});

update_run_history(app_id, id, {
type: "status",
endpoint: "/predict",
fn_index: 3,
queue: true,
stage: "error",
message: "generation failed"
});

expect(read_run_history(app_id)[0]).toMatchObject({
status: "failed",
error: "generation failed"
});
expect(read_run_history(app_id)[0].duration_ms).toBeGreaterThanOrEqual(0);
});

test("keeps only the newest 100 runs", () => {
for (let index = 0; index < 105; index++) {
start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: [index]
});
}

const runs = read_run_history(app_id);
expect(runs).toHaveLength(100);
expect(runs[0].inputs).toEqual([104]);
expect(runs.at(-1)?.inputs).toEqual([5]);
});

test("deletes an individual run", () => {
const first = start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["first"]
});
start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["second"]
});

delete_run_history(app_id, first!);

expect(read_run_history(app_id)).toHaveLength(1);
expect(read_run_history(app_id)[0].inputs).toEqual(["second"]);
});

test("notifies this tab when runs are added, deleted or cleared", () => {
let notified = 0;
const unsubscribe = on_run_history_change(() => notified++);

const id = start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["hello"]
});
expect(notified).toBe(1);

// Progress updates are not structural, so they must not notify.
update_run_history(app_id, id, {
type: "status",
endpoint: "/predict",
fn_index: 0,
queue: true,
stage: "complete"
});
expect(notified).toBe(1);

delete_run_history(app_id, id!);
expect(notified).toBe(2);

clear_run_history(app_id);
expect(notified).toBe(3);

unsubscribe();
start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["bye"]
});
expect(notified).toBe(3);
});

test("keeps a separate history per app id", () => {
start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["mine"]
});
start_run_history({
app_id: other_app,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["theirs"]
});

expect(read_run_history(app_id)[0].inputs).toEqual(["mine"]);
expect(read_run_history(other_app)[0].inputs).toEqual(["theirs"]);
});

test("removes staged replays when pruning an old app", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
start_run_history({
app_id: other_app,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["old"]
});
stage_run_history_replay(other_app, read_run_history(other_app)[0]);

for (const [index, replacement_app] of replacement_apps.entries()) {
vi.setSystemTime(new Date(Date.UTC(2025, 0, index + 2)));
start_run_history({
app_id: replacement_app,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: [index]
});
}

expect(read_run_history(other_app)).toEqual([]);
expect(consume_run_history_replay(other_app)).toBeNull();
});
});

describe("run history url", () => {
test("joins the app root and the api prefix", () => {
expect(run_history_url("http://localhost:7860")).toBe(
"http://localhost:7860/gradio_api/runs"
);
expect(run_history_url("http://localhost:7860/my-app/")).toBe(
"http://localhost:7860/my-app/gradio_api/runs"
);
expect(run_history_url("http://localhost:7860", "/api")).toBe(
"http://localhost:7860/api/runs"
);
});
});

describe.skipIf(in_browser)("run history outside the browser", () => {
test("is a no-op without storage", () => {
expect(
start_run_history({
app_id,
endpoint: "/predict",
api_name: "/predict",
fn_index: 0,
inputs: ["hello"]
})
).toBeNull();
expect(read_run_history(app_id)).toEqual([]);
expect(consume_run_history_replay(app_id)).toBeNull();
});
});
Loading
Loading