feat: implement needsAttention logic for interrupt handling in ThreadList

This commit is contained in:
X-iZhang
2026-06-30 06:13:14 +01:00
parent a954b6a314
commit 363f15d850
+30 -24
View File
@@ -222,6 +222,30 @@ export function ThreadList({
// arrives sorted by updated_at desc from the backend.
const pinned = useMemo(() => filtered.filter((t) => t.pinned), [filtered]);
// A thread belongs in "Requiring Attention" when its interrupt actually needs
// the user. Single source of truth for both the bucket and the count badge.
// - `needsUserInput` (an `ask_user` interrupt is active) - auto-approve can
// NOT answer these, so the row must surface no matter what.
// - A plain tool-approval interrupt that will NOT be auto-resolved: either
// auto-approve is off (the user has to approve it), OR this is not the
// currently-open thread. The auto-resume effect lives only in the mounted
// ChatInterface (the open thread), so a backgrounded auto-approve thread
// that hits a tool-approval interrupt is NOT resumed on its own - it must
// keep surfacing here until the user opens it (which triggers the resume).
// Only the open auto-approve thread is exempted: lifting it out of its time
// group would make it jump around for a behaviour the user opted out of, and
// it really will resume a moment later.
const needsAttention = useCallback(
(thread: ThreadItem): boolean => {
if (thread.needsUserInput) return true;
if (thread.status !== "interrupted") return false;
const willAutoResume =
thread.id === currentThreadId && getThreadAutoApprove(thread.id);
return !willAutoResume;
},
[currentThreadId]
);
// Group threads by time and status
const grouped = useMemo(() => {
const groups: Record<keyof typeof GROUP_LABELS, ThreadItem[]> = {
@@ -236,20 +260,7 @@ export function ThreadList({
// Pinned threads live in the "Research" section only, not the time groups.
if (thread.pinned) return;
// Bucket into "Requiring Attention" when the interrupt actually needs
// the user. Two cases qualify:
// - `needsUserInput` (an `ask_user` interrupt is active) - auto-approve
// can NOT handle these, so the row must surface no matter what.
// - Plain interrupt with auto-approve off - the user has to approve
// the tool call themselves.
// With auto-approve on AND no `ask_user`, the WebUI resumes the run on
// its own; lifting the row out of its time group would make it jump
// around in the sidebar for a behaviour the user has explicitly opted
// out of seeing.
if (
thread.needsUserInput ||
(thread.status === "interrupted" && !getThreadAutoApprove(thread.id))
) {
if (needsAttention(thread)) {
groups.interrupted.push(thread);
return;
}
@@ -269,18 +280,13 @@ export function ThreadList({
});
return groups;
}, [filtered, now]);
}, [filtered, now, needsAttention]);
const interruptedCount = useMemo(() => {
// Mirrors the `grouped` logic: count threads that actually need the user
// either via an ask_user interrupt OR a plain interrupt with auto-approve
// off. Auto-approve threads with tool-call interrupts skip the badge.
return flattened.filter(
(t) =>
t.needsUserInput ||
(t.status === "interrupted" && !getThreadAutoApprove(t.id))
).length;
}, [flattened]);
// Same predicate as the `grouped` bucket so the badge matches the visible
// group exactly.
return flattened.filter(needsAttention).length;
}, [flattened, needsAttention]);
// Expose thread list revalidation to parent component
// Use refs to create a stable callback that always calls the latest mutate function