File size: 2,737 Bytes
cc11e77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
 * useAgentFileChanged.ts — Fix D: watch mode preview signal listener
 *
 * Ascolta il CustomEvent "agent:file-changed" emesso da toolExecutor.ts
 * ogni volta che apply_patch / write_file / create_file / iterate_file
 * completano con successo (tool non-errore).
 *
 * Design:
 *   - Safari-safe: CustomEvent + addEventListener (nessun BroadcastChannel)
 *   - Zero-cost se enabled=false: nessun listener registrato
 *   - Debounce: batch di scritture rapide consecutive → un solo callback
 *   - Stable callback ref: il callback può aggiornarsi senza re-subscribe
 *
 * Emitter (toolExecutor.ts Fix D):
 *   window.dispatchEvent(new CustomEvent("agent:file-changed", { detail: { tool, t } }))
 *
 * @module useAgentFileChanged
 */

import { useEffect, useRef } from "react";

export interface AgentFileChangedDetail {
  /** Nome del tool che ha modificato il file (apply_patch, write_file, …) */
  tool: string;
  /** Timestamp Unix ms dell'evento */
  t:   number;
}

/**
 * Registra un listener per "agent:file-changed" con debounce.
 *
 * @param onChanged  — callback stabile: chiamata al massimo ogni `debounceMs`
 * @param debounceMs — debounce tra eventi ravvicinati (default 300ms)
 * @param enabled    — false = nessun listener (default true)
 *
 * @example
 *   // In PreviewSandbox: refresh automatico quando l'agente scrive un file
 *   useAgentFileChanged(
 *     useCallback(() => { if (html) runtime.refresh("auto"); }, [html, runtime]),
 *   );
 */
export function useAgentFileChanged(
  onChanged:   (detail: AgentFileChangedDetail) => void,
  debounceMs = 300,
  enabled    = true,
): void {
  const timerRef    = useRef<ReturnType<typeof setTimeout> | null>(null);
  // Stable callback ref — aggiornato ogni render senza rifare il subscribe
  const callbackRef = useRef(onChanged);
  useEffect(() => { callbackRef.current = onChanged; });

  useEffect(() => {
    if (!enabled || typeof window === "undefined") return;

    const handler = (e: Event) => {
      const detail = (e as CustomEvent<AgentFileChangedDetail>).detail
        ?? { tool: "unknown", t: Date.now() };
      // Debounce: cancella il timer precedente e rischedula
      if (timerRef.current !== null) clearTimeout(timerRef.current);
      timerRef.current = setTimeout(() => {
        timerRef.current = null;
        callbackRef.current(detail);
      }, debounceMs);
    };

    window.addEventListener("agent:file-changed", handler);
    return () => {
      window.removeEventListener("agent:file-changed", handler);
      if (timerRef.current !== null) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
    };
  }, [enabled, debounceMs]); // callbackRef è stabile — non serve in deps
}