import type { ClarificationRequestMetadata, Message } from "@/lib/types/http"; export function isPendingClarificationMessage(message: Message): boolean { if (message.type !== "clarification_request") return false; const metadata = message.metadata as ClarificationRequestMetadata | undefined; return !metadata?.status || metadata.status === "pending"; } export function findPendingClarification(messages?: readonly Message[] | null): Message | null { if (!messages) return null; for (let i = messages.length - 1; i >= 0; i--) { if (isPendingClarificationMessage(messages[i])) return messages[i]; } return null; } // findPendingClarificationGroup returns every clarification_request message // that shares the latest pending message's pending_id, ordered by chat position. // Multi-question bundles emit one message per question; the chat panel uses this // list to render every pending question card together. // // Gates on `question_total` from metadata: returns an empty array until the // number of messages received equals the expected bundle size. This prevents // a user from acting on a partially-arrived bundle (clicking an option before // the rest of the N messages have been streamed in via the WS), which would // otherwise trigger a 400 from the backend's all-required gate. export function findPendingClarificationGroup(messages?: readonly Message[] | null): Message[] { if (!messages) return []; const last = findPendingClarification(messages); if (!last) return []; const meta = last.metadata as ClarificationRequestMetadata | undefined; const pendingID = meta?.pending_id; if (!pendingID) return [last]; const group = messages.filter((m) => { if (m.type !== "clarification_request") return false; const mMeta = m.metadata as ClarificationRequestMetadata | undefined; return mMeta?.pending_id === pendingID; }); const expectedTotal = meta?.question_total; if (typeof expectedTotal === "number" && expectedTotal > 0 && group.length < expectedTotal) { return []; } return group; } export function hasPendingClarification(messages?: readonly Message[] | null): boolean { return findPendingClarification(messages) !== null; } export function hasPendingClarificationForSession( messagesBySession: Record, sessionId?: string | null, ): boolean { if (!sessionId) return false; return hasPendingClarification(messagesBySession[sessionId]); } // --- Permission request helpers --- export function isPendingPermissionMessage(message: Message): boolean { if (message.type !== "permission_request") return false; const metadata = message.metadata as { status?: string } | undefined; return !metadata?.status || metadata.status === "pending"; } // hasPendingPermissionRequest reports whether the *current turn* is blocked // on a permission_request. Scope: // - Scans backwards from the end and stops at the first permission_request // it sees — only the latest one drives the UI. A stale pending row left // behind by an earlier crash followed by a newer approved one must not // light up the amber icon (the agent is no longer blocked on the old row). // - Honours turn boundaries: when the latest message has a turn_id, walking // back to any row that doesn't share it ends the scan (including legacy // rows with null/undefined turn_id). Old turns' permissions can never // leak into the indicator and the scan stays bounded by the current turn // size (typically 5–50 messages) regardless of total session length. // - When the latest message itself has no turn_id (entirely pre-turn-scope // data), boundary enforcement is disabled and we fall back to plain // latest-only semantics across the whole array. export function hasPendingPermissionRequest(messages?: readonly Message[] | null): boolean { if (!messages?.length) return false; const latestTurnId = messages[messages.length - 1].turn_id; for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i]; if (latestTurnId && m.turn_id !== latestTurnId) break; if (m.type !== "permission_request") continue; return isPendingPermissionMessage(m); } return false; } export function hasPendingPermissionForSession( messagesBySession: Record, sessionId?: string | null, ): boolean { if (!sessionId) return false; return hasPendingPermissionRequest(messagesBySession[sessionId]); }