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>
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
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();
|
|
},
|
|
};
|
|
}
|