import type { Accessor } from "solid-js"; /** * Tracks whether a streaming response has stalled (no new tokens for a while) * and produces a smooth 0→1 intensity value suitable for color interpolation * (e.g. fading the spinner glyph toward red). * * Driven by an external clock signal rather than its own interval, so it * automatically slows down when the terminal is backgrounded (no ticks = no * updates) and doesn't create a second timer alongside the animation clock. * * ```ts * const time = useClock(50); * const stalled = useStalled(time, () => responseLength()); * // stalled.isStalled() — boolean * // stalled.intensity() — 0..1 * ``` */ export type StalledState = { isStalled: Accessor; intensity: Accessor; }; export function useStalled( time: Accessor, getLength: Accessor, opts?: { delayMs?: number; rampMs?: number }, ): StalledState { const delay = opts?.delayMs ?? 3000; const ramp = opts?.rampMs ?? 2000; let lastLength = getLength(); let lastChangeTime = 0; let smoothIntensity = 0; let lastSmoothTime = 0; const isStalled = (): boolean => { const t = time(); // subscribe to clock const len = getLength(); if (len > lastLength) { lastLength = len; lastChangeTime = t; } return t - lastChangeTime > delay; }; const intensity = (): number => { const t = time(); // subscribe to clock const len = getLength(); if (len > lastLength) { lastLength = len; lastChangeTime = t; smoothIntensity = 0; lastSmoothTime = t; return 0; } const idle = t - lastChangeTime; const target = idle > delay ? Math.min((idle - delay) / ramp, 1) : 0; // Smooth approach — step at ~50ms granularity const dt = t - lastSmoothTime; if (dt >= 50 && (target > 0 || smoothIntensity > 0)) { const diff = target - smoothIntensity; if (Math.abs(diff) < 0.01) { smoothIntensity = target; } else { smoothIntensity += diff * 0.1; } lastSmoothTime = t; } return smoothIntensity; }; return { isStalled, intensity }; }