perf(client): cut needless polling for local users

- ConversationView: an ended session (SessionDetail, SessionReplay pass
  `ended`) polls its transcript every 60 s instead of 3 s and stops
  rescanning sub-agents; new_event, reconnect and show still fetch at once.
- Workspace: GET /api/run follows run-moving WS messages (lane run
  columns, session created/status change, runs_interrupted) with a 30 s
  fallback, instead of a blind 5 s poll.
- StatusBar: session counts re-read only when a session's
  status/waiting/archived state changes (60 s fallback); costs re-read at
  most every 30 s while work is happening and every 2 min otherwise.
- Dashboard recent activity: 60 s safety poll (was 10 s) plus remote
  import messages; WS still prepends new events.
- New lib/sessionChange.ts matcher with tests.
This commit is contained in:
2026-10-16 10:15:26 +07:00
parent ecbbb4dd8f
commit 57c423cdb1
8 changed files with 187 additions and 76 deletions
@@ -14,6 +14,10 @@
* `onOpenTerminal`) and hears how full the context is (`onContext`) and
* whether its session has started (`onNotStarted`: no row yet — the CLI may
* be asking in the terminal, e.g. to trust the folder, which this then says).
* A caller that knows the session has ended passes `ended`: nothing more is
* being written, so the catch-up poll drops from 3 s to 60 s and the
* sub-agent list stops being rescanned — a `new_event` frame, a reconnect or
* the view being shown again still fetch the tail at once.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
/* =============================================================================
@@ -79,6 +83,9 @@ import type { TranscriptMessage, TranscriptInfo, WSMessage } from "../../lib/typ
// hook event. A short visibility-gated poll closes that gap and also rescues
// the conversation from missed/late WebSocket frames.
const POLL_INTERVAL_MS = 3000;
// An ended session writes nothing more: a slow safety poll only, in case it was
// resumed somewhere whose frames this view missed.
const ENDED_POLL_INTERVAL_MS = 60000;
// Rescan the transcripts list periodically so new subagents that spawn
// mid-session appear in the dropdown without a page reload.
const TRANSCRIPTS_REFRESH_MS = 15000;
@@ -110,6 +117,8 @@ interface ConversationViewProps {
onContext?: (tokens: number, model: string | null) => void;
/** A live pane's session has no row yet: the CLI has not started it. */
onNotStarted?: (notStarted: boolean) => void;
/** The session has ended (and no pane holds it): poll slowly, if at all. */
ended?: boolean;
}
function ConversationViewPane({
@@ -124,6 +133,7 @@ function ConversationViewPane({
stopped = 0,
onContext,
onNotStarted,
ended = false,
}: ConversationViewProps) {
const { t } = useTranslation("sessions");
const [messages, setMessages] = useState<TranscriptMessage[]>([]);
@@ -188,14 +198,18 @@ function ConversationViewPane({
}
}
loadTranscripts();
const interval = window.setInterval(() => {
if (isShown(onScreen)) loadTranscripts();
}, TRANSCRIPTS_REFRESH_MS);
// An ended session spawns no new sub-agents: the load above (re-run when
// it ends, for the last ones) is enough.
const interval = ended
? undefined
: window.setInterval(() => {
if (isShown(onScreen)) loadTranscripts();
}, TRANSCRIPTS_REFRESH_MS);
return () => {
cancelled = true;
window.clearInterval(interval);
};
}, [sessionId]);
}, [sessionId, ended]);
// Sync external initialTranscriptId to internal state
useEffect(() => {
@@ -355,9 +369,12 @@ function ConversationViewPane({
let interval: number | null = null;
function start() {
if (interval !== null) return;
interval = window.setInterval(() => {
if (isShown(onScreen)) fetchNewMessages();
}, POLL_INTERVAL_MS);
interval = window.setInterval(
() => {
if (isShown(onScreen)) fetchNewMessages();
},
ended ? ENDED_POLL_INTERVAL_MS : POLL_INTERVAL_MS
);
}
function stop() {
if (interval !== null) {
@@ -382,7 +399,7 @@ function ConversationViewPane({
stop();
document.removeEventListener("visibilitychange", onVisibility);
};
}, [fetchNewMessages, onScreen]);
}, [fetchNewMessages, onScreen, ended]);
// Manual refresh - surfaces a control in the toolbar so users can force
// a sync without reloading the page.