zz_

app/tui/useLadderTrees.ts

zzmq.dev@gmail.com/zz / app / tui / useLadderTrees.ts @ 5e5720fb Download
Ladder Summary
  • file The module provides a React hook `useLadderTrees` that periodically (default every 2 seconds) reads each repository’s resolved Ladder tree—either locally via `collectLadder` or remotely via `callRemoteTool`—and returns an array of `LadderSnapshot` objects (`{repo, tree}`) reflecting the latest state. It guards against overlapping fetches, skips repos that cannot be reached, and cleans up the interval when the component unmounts. Exported members: the `LadderSnapshot` interface and the `useLadderTrees` hook.
// Polls each watched repo's resolved Ladder tree (engine/ladderRender.ts's
// collectLadder -- the same resolver `zz ladder dump` uses) on an
// interval, so the TUI can show whether Ladder writes are actually
// landing, not just the Lane/Ask/Answer protocol trail.
import { useEffect, useState } from "react";
import { collectLadder, type ResolvedRung } from "../engine/ladderRender.js";
import { callRemoteTool } from "../cli/lib/remoteTools.js";

export interface LadderSnapshot {
  repo: string;
  tree: ResolvedRung;
}

export function useLadderTrees(repos: { repo: string; repoDir: string; stateDir: string }[], intervalMs = 2000, remote = false): LadderSnapshot[] {
  const [snapshots, setSnapshots] = useState<LadderSnapshot[]>([]);
  // See useLaneSnapshots.ts's identical comment -- content-keyed, not
  // identity-keyed, so a fresh array reference from the watch store on
  // every render doesn't restart polling unless the repo set actually changed.
  const reposKey = repos.map((r) => r.repo).join(",");

  useEffect(() => {
    let cancelled = false;
    // One round trip per repo now (ladder_tree, server-side resolved
    // and cached -- see cloudflareBackend.ts's readLadderTree), not
    // one per rung -- but still guarded against overlap, since a cold
    // cache on a large tree is still a real wait, and piling up
    // overlapping reads on top of that looks indistinguishable from
    // "never shows anything" rather than "takes a moment."
    let inFlight = false;
    const read = async () => {
      if (inFlight) return;
      inFlight = true;
      try {
        const next: LadderSnapshot[] = [];
        for (const { repo, repoDir, stateDir } of repos) {
          try {
            // allowAnonymous: see useLaneSnapshots.ts's identical comment.
            const tree = remote ? ((await callRemoteTool(repo, "ladder_tree", {}, true)) as ResolvedRung | null) : (await collectLadder(repoDir, stateDir)).tree;
            if (tree) next.push({ repo, tree });
          } catch {
            // repo not onboarded/bootstrapped/reachable yet -- skip rather than crash the view
          }
        }
        if (!cancelled) setSnapshots(next);
      } finally {
        inFlight = false;
      }
    };
    read();
    const id = setInterval(read, intervalMs);
    return () => {
      cancelled = true;
      clearInterval(id);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [intervalMs, remote, reposKey]);

  return snapshots;
}