"use client"; import Link from "next/link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { IconBrandGithub } from "@tabler/icons-react"; import { Alert, AlertDescription } from "@kandev/ui/alert"; import { PageTopbar } from "@/components/page-topbar"; import { useGitHubStatus } from "@/hooks/domains/github/use-github-status"; import type { Repository, Workflow, WorkflowStep } from "@/lib/types/http"; import type { GitHubIssue, GitHubPR } from "@/lib/types/github"; import { PRList } from "@/components/github/my-github/pr-list"; import { IssueList } from "@/components/github/my-github/issue-list"; import { PresetsSidebar, type SidebarSelection, } from "@/components/github/my-github/presets-sidebar"; import { PR_PRESETS, ISSUE_PRESETS, type PresetOption, } from "@/components/github/my-github/search-bar"; import { useGitHubSearch } from "@/components/github/my-github/use-github-search"; import { useDefaultQueryPresets, resolvePresetOptions, } from "@/components/github/my-github/use-default-query-presets"; import { useSavedPresets, type SavedPreset } from "@/components/github/my-github/use-saved-presets"; import { useKnownRepos, resetKnownReposStore } from "@/components/github/my-github/use-known-repos"; import { useCommittedQuery } from "@/components/github/my-github/use-committed-query"; import { ListToolbar } from "@/components/github/my-github/list-toolbar"; import { ResultsPagination } from "@/components/github/my-github/results-pagination"; import { SavePresetDialog } from "@/components/github/my-github/save-preset-dialog"; import { QuickTaskLauncher, type LaunchPayload, type TaskPreset, } from "@/components/github/my-github/quick-task-launcher"; import { resolvePRPresets, resolveIssuePresets, } from "@/components/github/my-github/action-presets"; import { useGitHubActionPresets } from "@/hooks/domains/github/use-github-action-presets"; type GitHubPageClientProps = { workspaceId?: string; workflows: Workflow[]; steps: WorkflowStep[]; repositories: Repository[]; }; function PageHeader() { return ( } /> ); } function NotAuthenticatedNotice() { return ( GitHub is not connected. Configure GitHub authentication (gh CLI or a Personal Access Token) in{" "} Settings → GitHub {" "} to see your pull requests and issues. ); } function NoWorkspaceNotice() { return (
No workspace configured. Create a workspace first to start tasks from PRs/issues.
); } function resolveTitle( selection: SidebarSelection, saved: SavedPreset[], prPresets: PresetOption[], issuePresets: PresetOption[], ): string { if (selection.source === "saved") { return saved.find((p) => p.id === selection.id)?.label ?? "Saved query"; } const presets = selection.kind === "pr" ? prPresets : issuePresets; return ( presets.find((p) => p.value === selection.id)?.label ?? (selection.kind === "pr" ? "Pull requests" : "Issues") ); } function ResultsList({ selection, items, loading, error, prPresets, issuePresets, onStartTask, }: { selection: SidebarSelection; items: Array; loading: boolean; error: string | null; prPresets: TaskPreset[]; issuePresets: TaskPreset[]; onStartTask: (payload: LaunchPayload) => void; }) { if (selection.kind === "pr") { return ( ); } return ( ); } type GitHubPageState = ReturnType; function useRepoOptions( selection: SidebarSelection, committedQuery: string, items: Array, repoFilter: string, ): string[] { const pageRepos = useMemo( () => items .filter((it) => it.repo_owner && it.repo_name) .map((it) => `${it.repo_owner}/${it.repo_name}`), [items], ); // Reset the accumulator whenever the query context changes (preset, saved, // custom query). Repo filter is deliberately excluded so narrowing doesn't // reset — that's the whole point of the accumulator. const reposResetKey = `${selection.kind}:${selection.source}:${selection.id}:${committedQuery.trim()}`; const knownRepos = useKnownRepos(reposResetKey, pageRepos); return useMemo(() => { const set = new Set(knownRepos); if (repoFilter) set.add(repoFilter); return Array.from(set).sort(); }, [knownRepos, repoFilter]); } function useResolvedQueryPresets() { const { prPresets: storedPr, issuePresets: storedIssue } = useDefaultQueryPresets(); const pr = useMemo(() => resolvePresetOptions(storedPr, PR_PRESETS), [storedPr]); const issue = useMemo(() => resolvePresetOptions(storedIssue, ISSUE_PRESETS), [storedIssue]); return { pr, issue }; } function useGitHubPageState() { const { pr: resolvedPrPresets, issue: resolvedIssuePresets } = useResolvedQueryPresets(); const [selection, setSelection] = useState(() => ({ kind: "pr", source: "preset", id: resolvedPrPresets[0]?.value ?? "", })); const { draft: customQuery, committed: committedQuery, setDraft: setCustomQuery, setImmediate: setQueryImmediate, commit: commitCustomQuery, } = useCommittedQuery(resolvedPrPresets[0]?.filter ?? ""); const [repoFilter, setRepoFilter] = useState(""); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const { presets: savedPresets, save: saveSavedPreset, remove: removeSavedPreset, } = useSavedPresets(); const presets = selection.kind === "pr" ? resolvedPrPresets : resolvedIssuePresets; const search = useGitHubSearch( selection.kind, presets, selection.source === "preset" ? selection.id : "", committedQuery, repoFilter, ); const repoOptions = useRepoOptions(selection, committedQuery, search.items, repoFilter); const title = useMemo( () => resolveTitle(selection, savedPresets, resolvedPrPresets, resolvedIssuePresets), [selection, savedPresets, resolvedPrPresets, resolvedIssuePresets], ); const onSelect = useCallback( (s: SidebarSelection) => { setSelection(s); if (s.source === "saved") { const found = savedPresets.find((p) => p.id === s.id); setQueryImmediate(found?.customQuery ?? ""); setRepoFilter(found?.repoFilter ?? ""); return; } const presetList = s.kind === "pr" ? resolvedPrPresets : resolvedIssuePresets; const preset = presetList.find((p) => p.value === s.id); setQueryImmediate(preset?.filter ?? ""); setRepoFilter(""); }, [savedPresets, setQueryImmediate, resolvedPrPresets, resolvedIssuePresets], ); const canSaveCurrent = customQuery.trim().length > 0 || repoFilter.length > 0; const suggestedLabel = customQuery.trim() || (repoFilter ? `In ${repoFilter}` : "Saved query"); const onOpenSaveDialog = () => { if (canSaveCurrent) setSaveDialogOpen(true); }; const onConfirmSave = (label: string) => { const created = saveSavedPreset({ kind: selection.kind, label, customQuery, repoFilter }); setSelection({ kind: selection.kind, source: "saved", id: created.id }); }; const onDeleteSaved = (id: string) => { removeSavedPreset(id); if (selection.source === "saved" && selection.id === id) { const fallbackPresets = selection.kind === "pr" ? resolvedPrPresets : resolvedIssuePresets; setSelection({ kind: selection.kind, source: "preset", id: fallbackPresets[0]?.value ?? "" }); setQueryImmediate(fallbackPresets[0]?.filter ?? ""); setRepoFilter(""); } }; return { selection, customQuery, committedQuery, setCustomQuery, commitCustomQuery, repoFilter, setRepoFilter, savedPresets, search, repoOptions, title, onSelect, canSaveCurrent, suggestedLabel, saveDialogOpen, setSaveDialogOpen, onOpenSaveDialog, onConfirmSave, onDeleteSaved, resolvedPrPresets, resolvedIssuePresets, }; } function AuthenticatedLayout({ workspaceId, state, prPresets, issuePresets, onStartTask, }: { workspaceId: string | undefined; state: GitHubPageState; prPresets: TaskPreset[]; issuePresets: TaskPreset[]; onStartTask: (payload: LaunchPayload) => void; }) { const { selection, search, repoOptions, title } = state; return (
{!workspaceId && }
); } export function GitHubPageClient({ workspaceId, workflows, steps, repositories, }: GitHubPageClientProps) { const { status, loaded } = useGitHubStatus(); const [launchPayload, setLaunchPayload] = useState(null); const state = useGitHubPageState(); const { presets: storedPresets } = useGitHubActionPresets(workspaceId ?? null); const prPresets = useMemo(() => resolvePRPresets(storedPresets), [storedPresets]); const issuePresets = useMemo(() => resolveIssuePresets(storedPresets), [storedPresets]); // Drop the module-level repo accumulator on page unmount so a later visit // doesn't inherit a stale set from the previous navigation. useEffect(() => resetKnownReposStore, []); const onStartTask = useCallback((payload: LaunchPayload) => setLaunchPayload(payload), []); const onCloseLaunch = useCallback(() => setLaunchPayload(null), []); const authed = !!status?.authenticated; return (
{!loaded &&
Checking GitHub status…
} {loaded && !authed && (
)} {loaded && authed && ( )}
); }