@assistant-ui/react
Version:
Open-source TypeScript/React library for building production-grade AI chat experiences
73 lines (72 loc) • 2.48 kB
JavaScript
"use client";
import { useEffect, useRef, useState } from "@assistant-ui/tap/react-shim";
import { useAuiState } from "@assistant-ui/store";
import { useShallowSelector } from "@assistant-ui/store/internal";
// src/unstable/useMessageStallDetection.ts
/**
* Detects mid-run output stalls on the current message: while the message is
* running, watches its text, reasoning, and tool-argument values plus tool-result
* availability and reports a stall once they stop changing for `thresholdMs`.
* Useful for re-surfacing a "still working" indicator during tool think-time or
* provider stalls, after the first tokens have already streamed.
*
* Must be used inside a message scope.
*
* @deprecated Experimental since 2026-06-13. Not scheduled for removal; the API may change in any release.
*/
function unstable_useMessageStallDetection(options) {
const thresholdMs = options?.thresholdMs ?? 2e3;
const activity = useAuiState(useShallowSelector((s) => {
if (!(s.message.status?.type === "running")) return [false];
const values = [true, s.message.content.length];
for (const part of s.message.content) if (part.type === "text" || part.type === "reasoning") values.push(part.type, part.text);
else if (part.type === "tool-call") values.push(part.type, part.argsText, part.result !== void 0);
else values.push(part.type);
return values;
}));
const running = activity[0] === true;
const lastActivityRef = useRef(0);
const [stalled, setStalled] = useState(false);
const [now, setNow] = useState(0);
useEffect(() => {
if (!running) return void 0;
lastActivityRef.current = Date.now();
}, [running, activity]);
useEffect(() => {
if (!running) {
setStalled(false);
return;
}
const stall = () => {
setNow(Date.now());
setStalled(true);
};
const sinceActivity = Date.now() - lastActivityRef.current;
if (sinceActivity >= thresholdMs) {
stall();
return;
}
setStalled(false);
const id = setTimeout(stall, thresholdMs - sinceActivity);
return () => clearTimeout(id);
}, [
running,
activity,
thresholdMs
]);
useEffect(() => {
if (!stalled) return void 0;
const id = setInterval(() => setNow(Date.now()), 1e3);
return () => clearInterval(id);
}, [stalled]);
if (!stalled) return {
stalled: false,
stalledForMs: 0
};
return {
stalled: true,
stalledForMs: Math.max(0, now - lastActivityRef.current)
};
}
export { unstable_useMessageStallDetection };
//# sourceMappingURL=useMessageStallDetection.js.map