From a954b6a3144a578b4a338e7d40631e395724ca8d Mon Sep 17 00:00:00 2001 From: jfilipiuk Date: Tue, 30 Jun 2026 07:05:42 +0200 Subject: [PATCH] fix: skip 'Requiring Attention' bucket for auto-approved threads (still pin on ask_user) (#24) Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com> --- src/app/components/ThreadList.tsx | 25 +++++++++++++++++++++++-- src/app/hooks/useThreads.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/app/components/ThreadList.tsx b/src/app/components/ThreadList.tsx index 90a388c..6285f1f 100644 --- a/src/app/components/ThreadList.tsx +++ b/src/app/components/ThreadList.tsx @@ -41,6 +41,7 @@ import { exportThread, } from "@/app/hooks/useThreads"; import { useMemoryActivity } from "@/app/hooks/useMemoryActivity"; +import { getThreadAutoApprove } from "@/lib/autoApprove"; import { Dialog, DialogContent, @@ -235,7 +236,20 @@ export function ThreadList({ // Pinned threads live in the "Research" section only, not the time groups. if (thread.pinned) return; - if (thread.status === "interrupted") { + // 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)) + ) { groups.interrupted.push(thread); return; } @@ -258,7 +272,14 @@ export function ThreadList({ }, [filtered, now]); const interruptedCount = useMemo(() => { - return flattened.filter((t) => t.status === "interrupted").length; + // 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]); // Expose thread list revalidation to parent component diff --git a/src/app/hooks/useThreads.ts b/src/app/hooks/useThreads.ts index 4634078..76b0ff1 100644 --- a/src/app/hooks/useThreads.ts +++ b/src/app/hooks/useThreads.ts @@ -11,6 +11,11 @@ export interface ThreadItem { description: string; assistantId?: string; pinned: boolean; + /** True when any of the thread's pending interrupts is an `ask_user` — + * i.e. the agent is asking the user a question that auto-approve can't + * resolve. Used by the sidebar to keep these threads in "Requiring + * Attention" even when auto-approve is on. */ + needsUserInput: boolean; } const DEFAULT_PAGE_SIZE = 20; @@ -163,6 +168,28 @@ export function useThreads(props: { (thread.metadata as Record | undefined)?.pinned === true; + // Walk `thread.interrupts` (Record) and flag any + // value with `type: "ask_user"`. The auto-approver can't resolve those, + // so the sidebar should keep the row in "Requiring Attention" even when + // auto-approve is on for the thread. + let needsUserInput = false; + const interrupts = thread.interrupts as + | Record> + | undefined; + if (interrupts && typeof interrupts === "object") { + for (const list of Object.values(interrupts)) { + if (!Array.isArray(list)) continue; + for (const ir of list) { + const value = ir?.value as { type?: unknown } | undefined; + if (value && value.type === "ask_user") { + needsUserInput = true; + break; + } + } + if (needsUserInput) break; + } + } + return { id: thread.thread_id, updatedAt: new Date(thread.updated_at), @@ -171,6 +198,7 @@ export function useThreads(props: { description, assistantId, pinned, + needsUserInput, }; }); },