feat: integrate HITL phase management across tool components

- Refactored ApprovalCard in various tools (Gmail, Google Calendar, Google Drive) to utilize the new useHitlPhase hook for improved state management.
- Updated logic to handle tool action phases (pending, processing, complete, rejected) consistently across components, enhancing user feedback during interactions.
- Simplified decision handling by removing direct state management for approval decisions, streamlining the approval process.
- Enhanced UI feedback to reflect the current phase of tool actions, improving user experience during tool interactions.
This commit is contained in:
Anish Sarkar 2026-03-21 11:18:35 +05:30
parent ff6514a99f
commit 9cd2c1f712
19 changed files with 706 additions and 590 deletions

View file

@ -0,0 +1,62 @@
import { useEffect, useRef, useState } from "react";
export type HitlPhase = "pending" | "processing" | "complete" | "rejected";
interface HitlInterruptLike {
__decided__?: string | null;
__completed__?: boolean;
}
const MINIMUM_SHIMMER_MS = 500;
const FALLBACK_TIMEOUT_MS = 30_000;
/**
* State machine for HITL approval card phases.
*
* Phases:
* pending waiting for user decision (show buttons)
* processing user approved/edited, waiting for backend (shimmer)
* complete backend responded with __completed__ (done text)
* rejected user rejected (cancelled text)
*/
export function useHitlPhase(interruptData: HitlInterruptLike): {
phase: HitlPhase;
setProcessing: () => void;
setRejected: () => void;
} {
const [phase, setPhase] = useState<HitlPhase>(() => {
if (interruptData.__decided__ === "reject") return "rejected";
if (interruptData.__decided__) return "complete";
return "pending";
});
const shimmerStartRef = useRef<number | null>(null);
// processing → complete when __completed__ arrives (with min shimmer duration)
useEffect(() => {
if (phase !== "processing") return;
if (!interruptData.__completed__) return;
const elapsed = shimmerStartRef.current ? Date.now() - shimmerStartRef.current : Infinity;
const remaining = Math.max(0, MINIMUM_SHIMMER_MS - elapsed);
const timer = setTimeout(() => setPhase("complete"), remaining);
return () => clearTimeout(timer);
}, [phase, interruptData.__completed__]);
// Fallback: processing → complete after 30s even if __completed__ never arrives
useEffect(() => {
if (phase !== "processing") return;
const fallback = setTimeout(() => setPhase("complete"), FALLBACK_TIMEOUT_MS);
return () => clearTimeout(fallback);
}, [phase]);
return {
phase,
setProcessing: () => {
shimmerStartRef.current = Date.now();
setPhase("processing");
},
setRejected: () => setPhase("rejected"),
};
}