import { computed, onScopeDispose, shallowRef, watch } from "vue"; import type { ComputedRef, Ref } from "vue"; import { useCopilotKit } from "../providers/useCopilotKit"; import { useAgent } from "./use-agent"; import type { InterruptEvent, InterruptHandlerProps, InterruptRenderProps, } from "../types/interrupt"; export type { InterruptEvent, InterruptHandlerProps, InterruptRenderProps }; const INTERRUPT_EVENT_NAME = "on_interrupt"; type InterruptHandlerFn = ( props: InterruptHandlerProps, ) => TResult | PromiseLike; type InterruptResultFromHandler = THandler extends ( ...args: never[] ) => infer TResult ? TResult extends PromiseLike ? TResolved | null : TResult | null : null; type InterruptResult = InterruptResultFromHandler< InterruptHandlerFn >; export interface UseInterruptConfig { handler?: InterruptHandlerFn; enabled?: (event: InterruptEvent) => boolean; agentId?: string; renderInChat?: boolean; } export interface UseInterruptResult { interrupt: Ref | null>; result: Ref>; hasInterrupt: ComputedRef; resolveInterrupt: (response: unknown) => void; slotProps: ComputedRef > | null>; } export function isPromiseLike( value: TValue | PromiseLike, ): value is PromiseLike { return ( (typeof value === "object" || typeof value === "function") && value !== null && typeof Reflect.get(value, "then") === "function" ); } function normalizeAsyncResult( value: PromiseLike, ): Promise { if (value instanceof Promise) { return value; } return new Promise((resolve, reject) => { try { value.then(resolve, reject); } catch (error) { reject(error); } }); } /** * Vue composable for handling `on_interrupt` custom events from an agent. * * It tracks the latest pending interrupt, optionally derives UI data via * `handler`, and can publish slot state into `CopilotChat` so consumers render * interrupts through the `#interrupt` slot instead of render functions/TSX. * * @example * ```ts * const { interrupt, hasInterrupt, resolveInterrupt } = useInterrupt({ * handler: ({ event }) => ({ label: String(event.value) }), * }); * ``` */ export function useInterrupt( config: UseInterruptConfig = {}, ): UseInterruptResult { const { copilotkit } = useCopilotKit(); const { agent } = useAgent({ agentId: config.agentId }); const interrupt = shallowRef | null>(null); const result = shallowRef>(null); watch( agent, (resolvedAgent, _previousAgent, onCleanup) => { if (!resolvedAgent) { interrupt.value = null; result.value = null; return; } let localInterrupt: InterruptEvent | null = null; const subscription = resolvedAgent.subscribe({ onCustomEvent: ({ event }) => { if (event.name === INTERRUPT_EVENT_NAME) { localInterrupt = { name: event.name, value: event.value as TValue, }; } }, onRunStartedEvent: () => { localInterrupt = null; interrupt.value = null; }, onRunFinalized: () => { if (localInterrupt) { interrupt.value = localInterrupt; localInterrupt = null; } }, onRunFailed: () => { localInterrupt = null; }, }); onCleanup(() => subscription.unsubscribe()); }, { immediate: true }, ); const resolveInterrupt = (response: unknown) => { const resolvedAgent = agent.value; if (!resolvedAgent) return; const interruptEventValue = interrupt.value?.value; interrupt.value = null; void copilotkit.value .runAgent({ agent: resolvedAgent, forwardedProps: { command: { resume: response, interruptEvent: interruptEventValue, }, }, }) .catch((error) => { console.error( "[CopilotKit] useInterrupt: failed to resume agent:", error, ); }); }; watch( interrupt, (pendingEvent, _previous, onCleanup) => { if (!pendingEvent) { result.value = null; return; } if (config.enabled && !config.enabled(pendingEvent)) { result.value = null; return; } const handler = config.handler; if (!handler) { result.value = null; return; } let cancelled = false; const maybePromise = handler({ event: pendingEvent, resolve: resolveInterrupt, }); if (isPromiseLike(maybePromise)) { normalizeAsyncResult(maybePromise) .then((resolved) => { if (!cancelled) { result.value = resolved; } }) .catch((err) => { if (!cancelled) { console.error("[CopilotKit] useInterrupt handler failed:", err); result.value = null; } }); } else { result.value = maybePromise; } onCleanup(() => { cancelled = true; }); }, { immediate: true }, ); const slotProps = computed > | null>(() => { if (!interrupt.value) return null; if (config.enabled && !config.enabled(interrupt.value)) return null; return { event: interrupt.value, result: result.value, resolve: resolveInterrupt, }; }); watch( [() => copilotkit.value, slotProps, () => config.renderInChat !== false], ([core, nextSlotProps, shouldRenderInChat], _previous, onCleanup) => { if (!shouldRenderInChat) { return; } core.setInterruptState( nextSlotProps as InterruptRenderProps | null, ); const publishedState = nextSlotProps; onCleanup(() => { if (core.interruptState === publishedState) { core.setInterruptState(null); } }); }, { immediate: true }, ); onScopeDispose(() => { const core = copilotkit.value; if ( config.renderInChat !== false && core.interruptState === slotProps.value ) { core.setInterruptState(null); } }); return { interrupt: interrupt as Ref | null>, result: result as Ref>, hasInterrupt: computed(() => slotProps.value !== null), resolveInterrupt, slotProps: slotProps as ComputedRef > | null>, }; }