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>
This commit is contained in:
jfilipiuk
2026-06-30 07:05:42 +02:00
committed by GitHub
parent fd60984048
commit a954b6a314
2 changed files with 51 additions and 2 deletions
+23 -2
View File
@@ -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
+28
View File
@@ -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<string, unknown> | undefined)?.pinned ===
true;
// Walk `thread.interrupts` (Record<task_id, Interrupt[]>) 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<string, Array<{ value?: unknown }>>
| 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,
};
});
},