mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-21 10:22:18 +00:00
Merge pull request #48950 from kshitijk4poor/salvage/dashboard-sessions-realtime
This commit is contained in:
commit
9cd7b8ca47
5 changed files with 102 additions and 7 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
web/src/lib/session-refresh.test.ts
Normal file
21
web/src/lib/session-refresh.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
26
web/src/lib/session-refresh.ts
Normal file
26
web/src/lib/session-refresh.ts
Normal 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
|
||||
);
|
||||
}
|
||||
|
|
@ -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
16
web/vitest.config.ts
Normal 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}"],
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue