feat(dashboard): 60s base refresh with event-driven immediate updates
Raise the status dashboard base refresh from 10s to 60s, and refresh immediately (debounced 1.5s) on events between ticks: - a real chat message arrives in a registered room (index.ts onMessage) - an agent starts/finishes a run, i.e. activity moves between rooms (GroupQueue.setOnActivityChange fired on activeCount changes) requestImmediateStatusUpdate() drives updateStatus out-of-band via a coalescing trigger. The existing re-entrancy guard means the base periodic refresh does not run while an edit-retry is in flight; a refresh requested during that window is remembered and runs once afterward. Adds createCoalescingTrigger with tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -111,12 +111,28 @@ let channelMetaLastRefresh = 0;
|
||||
let dashboardUpdateLogged = false;
|
||||
/** Guards against overlapping status updates while a slow edit-retry runs. */
|
||||
let statusUpdateRunning = false;
|
||||
/** A refresh was requested while one was already running; run once more after. */
|
||||
let statusUpdatePending = false;
|
||||
/**
|
||||
* On a failed status-message edit, retry the edit this many extra times at
|
||||
* STATUS_EDIT_RETRY_DELAY_MS spacing before falling back to a fresh message.
|
||||
*/
|
||||
const STATUS_EDIT_MAX_RETRIES = 2;
|
||||
const STATUS_EDIT_RETRY_DELAY_MS = 15_000;
|
||||
/** Coalesce bursts of event-driven refresh requests into one update. */
|
||||
const IMMEDIATE_UPDATE_DEBOUNCE_MS = 1_500;
|
||||
/** Set by the renderer's startUnifiedDashboard so external events can nudge it. */
|
||||
let immediateUpdateTrigger: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* Request an out-of-band status refresh (e.g. a new chat message arrived or an
|
||||
* agent's activity changed) so the dashboard updates immediately instead of
|
||||
* waiting for the next periodic tick. No-op until the renderer dashboard starts;
|
||||
* bursts are debounced.
|
||||
*/
|
||||
export function requestImmediateStatusUpdate(): void {
|
||||
immediateUpdateTrigger?.();
|
||||
}
|
||||
/** Codex service only: cached usage rows written into the status snapshot. */
|
||||
let cachedCodexUsageRows: UsageRow[] = [];
|
||||
/** Codex service only: ISO timestamp of last successful usage fetch. */
|
||||
@@ -801,6 +817,29 @@ export async function editStatusMessageWithRetry(args: {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a trigger that runs `run` at most once per `delayMs` window: the
|
||||
* first call schedules a run after `delayMs`, and further calls within that
|
||||
* window are folded into the same pending run. Timer fns are injectable for
|
||||
* tests.
|
||||
*/
|
||||
export function createCoalescingTrigger(
|
||||
run: () => void,
|
||||
delayMs: number,
|
||||
timer: {
|
||||
set: (cb: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
||||
} = { set: (cb, ms) => setTimeout(cb, ms) },
|
||||
): () => void {
|
||||
let scheduled: ReturnType<typeof setTimeout> | null = null;
|
||||
return () => {
|
||||
if (scheduled) return;
|
||||
scheduled = timer.set(() => {
|
||||
scheduled = null;
|
||||
run();
|
||||
}, delayMs);
|
||||
};
|
||||
}
|
||||
|
||||
export async function startUnifiedDashboard(
|
||||
opts: UnifiedDashboardOptions,
|
||||
): Promise<void> {
|
||||
@@ -836,8 +875,13 @@ export async function startUnifiedDashboard(
|
||||
writeLocalStatusSnapshot(opts);
|
||||
if (!isRenderer) return;
|
||||
// A failed edit now retries with delays, so a single updateStatus can run
|
||||
// for tens of seconds. Skip overlapping interval ticks to avoid double posts.
|
||||
if (statusUpdateRunning) return;
|
||||
// for tens of seconds. Skip overlapping ticks (this is also why the base
|
||||
// periodic refresh does not fire mid-retry); remember the request so it runs
|
||||
// once the in-flight update finishes.
|
||||
if (statusUpdateRunning) {
|
||||
statusUpdatePending = true;
|
||||
return;
|
||||
}
|
||||
statusUpdateRunning = true;
|
||||
|
||||
try {
|
||||
@@ -926,9 +970,22 @@ export async function startUnifiedDashboard(
|
||||
statusMessageId = null;
|
||||
} finally {
|
||||
statusUpdateRunning = false;
|
||||
if (statusUpdatePending) {
|
||||
statusUpdatePending = false;
|
||||
immediateUpdateTrigger?.();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (isRenderer) {
|
||||
// Event-driven refresh: external callers (new chat message, agent activity
|
||||
// change) call requestImmediateStatusUpdate(); bursts are coalesced.
|
||||
immediateUpdateTrigger = createCoalescingTrigger(
|
||||
() => void updateStatus(),
|
||||
IMMEDIATE_UPDATE_DEBOUNCE_MS,
|
||||
);
|
||||
}
|
||||
|
||||
setInterval(updateStatus, opts.statusUpdateInterval);
|
||||
setInterval(() => {
|
||||
if (!isRenderer || !statusMessageId) return;
|
||||
|
||||
Reference in New Issue
Block a user