v1.8.1: global agents + parser robustness + WS reconnect toast
Builtins move out of code into /data/AGENTS.md (always-on, mounted ro
into the container); per-project AGENTS.md is now an optional override.
agents.ts merges global + project entries with project-wins-by-name and
caches per-source mtimes (60s TTL). Parser switches to per-block
try/catch and returns AgentsResponse { agents, errors[] } so one
malformed block no longer fails the file. AgentPicker shows a
non-blocking amber chip listing skipped blocks and only fires a gray
toast when zero agents loaded.
WS reconnect UX (useUserEvents + useSessionStream) now silent on the
first disconnect; createWsReconnectToast escalates to gray after 3
failures or 15 s, then to red with a Retry Now action after 60 s.
useSessionStream also gained the exponential-backoff reconnect it was
missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
apps/web/src/hooks/wsReconnectToast.ts
Normal file
95
apps/web/src/hooks/wsReconnectToast.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// v1.8.1 thresholds. First disconnect is silent — mobile Authelia idle timeouts
|
||||
// and tab suspensions trip reconnects constantly and the old red "websocket
|
||||
// error" toast made the app feel broken. Only escalate once the failure is
|
||||
// sustained.
|
||||
const TOAST_AFTER_FAILS = 3;
|
||||
const TOAST_AFTER_MS = 15_000;
|
||||
const PERSISTENT_AFTER_MS = 60_000;
|
||||
|
||||
export interface WsReconnectToast {
|
||||
onFailure(): void;
|
||||
onConnected(): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
label: string; // shown in the toast (e.g. "Live updates")
|
||||
onRetryNow: () => void; // user clicked the "Retry now" action
|
||||
}
|
||||
|
||||
// Per-connection toast wrapper. Caller drives it from the WS lifecycle:
|
||||
// onFailure — after each failed connection attempt
|
||||
// onConnected — after a successful onopen
|
||||
// dispose — on hook unmount
|
||||
// The wrapper itself runs no timers and does not change the caller's reconnect
|
||||
// cadence; it only decides when to show / dismiss the toast.
|
||||
export function createWsReconnectToast(opts: Options): WsReconnectToast {
|
||||
let firstFailureAt: number | null = null;
|
||||
let failureCount = 0;
|
||||
let reconnectingId: string | number | null = null;
|
||||
let persistentId: string | number | null = null;
|
||||
|
||||
function dismissReconnecting(): void {
|
||||
if (reconnectingId !== null) {
|
||||
toast.dismiss(reconnectingId);
|
||||
reconnectingId = null;
|
||||
}
|
||||
}
|
||||
function dismissPersistent(): void {
|
||||
if (persistentId !== null) {
|
||||
toast.dismiss(persistentId);
|
||||
persistentId = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
onFailure() {
|
||||
if (firstFailureAt === null) firstFailureAt = Date.now();
|
||||
failureCount += 1;
|
||||
const elapsed = Date.now() - firstFailureAt;
|
||||
|
||||
// Escalate to red error + Retry button after PERSISTENT_AFTER_MS. Replaces
|
||||
// the gray toast if it's still showing.
|
||||
if (persistentId === null && elapsed >= PERSISTENT_AFTER_MS) {
|
||||
dismissReconnecting();
|
||||
persistentId = toast.error(`${opts.label}: connection lost`, {
|
||||
duration: Infinity,
|
||||
action: {
|
||||
label: 'Retry now',
|
||||
onClick: () => {
|
||||
dismissReconnecting();
|
||||
dismissPersistent();
|
||||
opts.onRetryNow();
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Gray "reconnecting…" toast once we've crossed either threshold.
|
||||
if (
|
||||
reconnectingId === null &&
|
||||
persistentId === null &&
|
||||
(failureCount >= TOAST_AFTER_FAILS || elapsed >= TOAST_AFTER_MS)
|
||||
) {
|
||||
reconnectingId = toast.warning(`${opts.label}: reconnecting…`, {
|
||||
duration: Infinity,
|
||||
});
|
||||
}
|
||||
},
|
||||
onConnected() {
|
||||
firstFailureAt = null;
|
||||
failureCount = 0;
|
||||
dismissReconnecting();
|
||||
dismissPersistent();
|
||||
},
|
||||
dispose() {
|
||||
firstFailureAt = null;
|
||||
failureCount = 0;
|
||||
dismissReconnecting();
|
||||
dismissPersistent();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user