123 lines
3.8 KiB
TypeScript
123 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useCallback, useRef } from "react";
|
|
import { listTaskPRs, getTaskPR } from "@/lib/api/domains/github-api";
|
|
import { getWebSocketClient } from "@/lib/ws/connection";
|
|
import { useAppStore } from "@/components/state-provider";
|
|
import type { TaskPR } from "@/lib/types/github";
|
|
|
|
/** Fetch and cache PR associations for a batch of task IDs. */
|
|
export function useTaskPRs(taskIds: string[]) {
|
|
const byTaskId = useAppStore((state) => state.taskPRs.byTaskId);
|
|
const loading = useAppStore((state) => state.taskPRs.loading);
|
|
const setTaskPRs = useAppStore((state) => state.setTaskPRs);
|
|
const setTaskPRsLoading = useAppStore((state) => state.setTaskPRsLoading);
|
|
|
|
useEffect(() => {
|
|
if (taskIds.length === 0 || loading) return;
|
|
setTaskPRsLoading(true);
|
|
listTaskPRs(taskIds, { cache: "no-store" })
|
|
.then((response) => {
|
|
setTaskPRs(response?.task_prs ?? {});
|
|
})
|
|
.catch(() => {
|
|
// Keep existing data on error
|
|
})
|
|
.finally(() => {
|
|
setTaskPRsLoading(false);
|
|
});
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [taskIds.join(",")]);
|
|
|
|
return { byTaskId, loading };
|
|
}
|
|
|
|
/** Fetch a single task's PR association. */
|
|
export function useTaskPR(taskId: string | null) {
|
|
const pr = useAppStore((state) => (taskId ? (state.taskPRs.byTaskId[taskId] ?? null) : null));
|
|
const setTaskPR = useAppStore((state) => state.setTaskPR);
|
|
|
|
const refresh = useCallback(() => {
|
|
if (!taskId) return;
|
|
getTaskPR(taskId, { cache: "no-store" })
|
|
.then((response) => {
|
|
if (response) setTaskPR(taskId, response);
|
|
})
|
|
.catch(() => {
|
|
// Ignore - PR may not exist for this task
|
|
});
|
|
}, [taskId, setTaskPR]);
|
|
|
|
useEffect(() => {
|
|
if (!taskId || pr) return;
|
|
refresh();
|
|
}, [taskId, pr, refresh]);
|
|
|
|
return { pr, refresh } as { pr: TaskPR | null; refresh: () => void };
|
|
}
|
|
|
|
/** Read the active task's PR from the store (no fetching). */
|
|
export function useActiveTaskPR(): TaskPR | null {
|
|
return useAppStore((s) => {
|
|
const taskId = s.tasks.activeTaskId;
|
|
return taskId ? (s.taskPRs.byTaskId[taskId] ?? null) : null;
|
|
});
|
|
}
|
|
|
|
const PR_DETECTION_INTERVAL = 30_000; // 30 seconds
|
|
|
|
/**
|
|
* Periodically checks for a PR on the session's branch when no PR is
|
|
* associated with the task yet. Triggers a backend check that ensures
|
|
* a PR watch exists and tries to find the PR immediately.
|
|
*/
|
|
export function useTaskPRDetection(
|
|
taskId: string | null,
|
|
sessionId: string | null,
|
|
branch: string | null,
|
|
) {
|
|
const pr = useAppStore((s) => (taskId ? (s.taskPRs.byTaskId[taskId] ?? null) : null));
|
|
const setTaskPR = useAppStore((s) => s.setTaskPR);
|
|
const checkingRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (!taskId || !sessionId || !branch || pr) return;
|
|
|
|
const check = () => {
|
|
if (checkingRef.current) return;
|
|
checkingRef.current = true;
|
|
|
|
const client = getWebSocketClient();
|
|
if (!client) {
|
|
checkingRef.current = false;
|
|
return;
|
|
}
|
|
|
|
client
|
|
.request<{ found?: boolean }>("github.check_session_pr", {
|
|
task_id: taskId,
|
|
session_id: sessionId,
|
|
})
|
|
.then((resp) => {
|
|
if (resp?.found) {
|
|
// PR was found and associated — fetch the full TaskPR record
|
|
getTaskPR(taskId, { cache: "no-store" })
|
|
.then((taskPr) => {
|
|
if (taskPr) setTaskPR(taskId, taskPr);
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
})
|
|
.catch(() => {})
|
|
.finally(() => {
|
|
checkingRef.current = false;
|
|
});
|
|
};
|
|
|
|
// Check immediately, then on interval
|
|
check();
|
|
const interval = setInterval(check, PR_DETECTION_INTERVAL);
|
|
return () => clearInterval(interval);
|
|
}, [taskId, sessionId, branch, pr, setTaskPR]);
|
|
}
|