Merge pull request #48950 from kshitijk4poor/salvage/dashboard-sessions-realtime

This commit is contained in:
kshitij 2026-06-19 19:09:34 +05:30 committed by GitHub
commit 9cd7b8ca47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 102 additions and 7 deletions

View file

@ -8,7 +8,8 @@
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"typecheck": "tsc -p . --noEmit"
"typecheck": "tsc -p . --noEmit",
"test": "vitest run"
},
"dependencies": {
"@nous-research/ui": "0.18.2",
@ -48,6 +49,7 @@
"three": "^0.180.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.56.1",
"vite": "^8.0.16"
"vite": "^8.0.16",
"vitest": "^4.1.5"
}
}

View file

@ -0,0 +1,21 @@
import { describe, it, expect } from "vitest";
import { shouldRefreshSessions } from "./session-refresh";
describe("shouldRefreshSessions", () => {
it("returns false on the first poll (no baseline yet)", () => {
expect(shouldRefreshSessions(null, "s2")).toBe(false);
});
it("returns false when the current response has no sessions", () => {
expect(shouldRefreshSessions("s1", null)).toBe(false);
expect(shouldRefreshSessions(null, null)).toBe(false);
});
it("returns false when the newest session id is unchanged", () => {
expect(shouldRefreshSessions("s1", "s1")).toBe(false);
});
it("returns true when a new session appears at the head of the list", () => {
expect(shouldRefreshSessions("s1", "s2")).toBe(true);
});
});

View file

@ -0,0 +1,26 @@
/**
* Decide whether the paginated sessions list should be silently
* re-fetched after an overview poll.
*
* The dashboard's FastAPI server and a terminal CLI are separate
* processes that share the same SQLite session DB. There is no
* inter-process push channel, so the Sessions page polls the 50 newest
* sessions every few seconds (the "overview" poll). When that poll
* surfaces a session id at the head of the list that we have not seen
* before, a new session was created in another process and the
* paginated list is stale refresh it.
*
* Returns false on the very first poll (no baseline yet) and when
* either id is null (empty DB / transient empty response), so we never
* trigger a spurious reload on mount or while the DB is empty.
*/
export function shouldRefreshSessions(
prevNewestId: string | null,
currentNewestId: string | null,
): boolean {
return (
prevNewestId !== null &&
currentNewestId !== null &&
prevNewestId !== currentNewestId
);
}

View file

@ -30,6 +30,7 @@ import {
Archive,
} from "lucide-react";
import { api } from "@/lib/api";
import { shouldRefreshSessions } from "@/lib/session-refresh";
import type {
SessionInfo,
SessionMessage,
@ -805,8 +806,12 @@ export default function SessionsPage() {
};
}, [setEnd]);
const loadSessions = useCallback((p: number) => {
setLoading(true);
const loadSessions = useCallback((p: number, silent = false) => {
// ``silent`` skips the loading spinner so background refreshes
// (triggered when the overview poll detects a new session from
// another process) don't flicker the whole page or drop the user's
// scroll position.
if (!silent) setLoading(true);
api
.getSessions(PAGE_SIZE, p * PAGE_SIZE)
.then((resp) => {
@ -814,7 +819,9 @@ export default function SessionsPage() {
setTotal(resp.total);
})
.catch(() => {})
.finally(() => setLoading(false));
.finally(() => {
if (!silent) setLoading(false);
});
}, []);
const loadStats = useCallback(() => {
@ -828,6 +835,15 @@ export default function SessionsPage() {
loadStats();
}, [loadStats]);
// Refs for the overview poll's new-session detection. The poll effect
// below is mounted once with stable deps, so it reads the current page
// and the last-seen newest session id through refs instead of capturing
// stale values. ``newestSeenRef`` starts null so the first poll sets a
// baseline without triggering a redundant reload (mount already loads).
const newestSeenRef = useRef<string | null>(null);
const pageRef = useRef(page);
pageRef.current = page;
useEffect(() => {
loadSessions(page);
refreshEmptyCount();
@ -841,13 +857,27 @@ export default function SessionsPage() {
.catch(() => {});
api
.getSessions(50)
.then((r) => setOverviewSessions(r.sessions))
.then((r) => {
setOverviewSessions(r.sessions);
// The dashboard server and a terminal CLI are separate
// processes sharing one session DB — there is no push channel,
// so we detect sessions created in another process here. The
// overview poll already fetches the 50 newest sessions, so we
// reuse its head id as a cheap change signal: when it changes,
// silently refresh the paginated list so the new session shows
// up in real time without a visible loading flicker.
const newest = r.sessions[0]?.id ?? null;
if (shouldRefreshSessions(newestSeenRef.current, newest)) {
loadSessions(pageRef.current, true);
}
newestSeenRef.current = newest;
})
.catch(() => {});
};
loadOverview();
const id = setInterval(loadOverview, 5000);
return () => clearInterval(id);
}, []);
}, [loadSessions]);
useEffect(() => {
const el = logScrollRef.current;

16
web/vitest.config.ts Normal file
View file

@ -0,0 +1,16 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
test: {
environment: "node",
include: ["src/**/*.test.{ts,tsx}"],
},
});