import { useId, useState } from "react"; import type { GithubFilter, GithubItem, GithubKind, GithubList } from "@ace/channel/protocol"; import { Button, buttonVariants, Input, ProjectSidebar, type SessionSidebarRepo, useLayoutLeft, useLocalStorage, useMedia, } from "@ace/ui"; import { IconCircleCheck, IconExternal, IconHash, IconIssue, IconListTree, IconLoader, IconMerge, IconPullRequest, IconPullRequestClosed, IconRotate, IconSearch, IconSidebar, } from "@ace/ui/icons"; import { type Query, useGithub } from "./github-cache"; import { host } from "./host"; import { type AppProject, checkoutId } from "./projects"; const NAMES = { issues: "Issues", prs: "Pull requests" }; const DATE = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", year: "numeric", }); type Filter = { state: GithubFilter; search: string; limit: number }; const DEFAULT_FILTER: Filter = { state: "open", search: "", limit: 50 }; const ICONS = { issues: { open: IconIssue, closed: IconCircleCheck, merged: IconMerge, draft: IconIssue }, prs: { open: IconPullRequest, closed: IconPullRequestClosed, merged: IconMerge, draft: IconPullRequest, }, }; const COLORS = { issues: { open: "text-accent", closed: "text-merged", merged: "text-merged", draft: "text-muted-foreground", }, prs: { open: "text-accent", closed: "text-destructive", merged: "text-merged", draft: "text-muted-foreground", }, }; function useGithubFilter(kind: GithubKind, project: AppProject) { return useLocalStorage( `ace:github-filter:${host.url}:${checkoutId(project.host, project.path)}:${kind}`, DEFAULT_FILTER, ); } export function GithubSidebar({ kind, project, repos, connected, onProject, onOpen }: { kind: GithubKind; project: AppProject; repos: SessionSidebarRepo[]; connected: boolean; onProject: (id: string) => void; onOpen: () => void; }) { const [filter, setFilter] = useGithubFilter(kind, project); const left = useLayoutLeft(); const phone = useMedia("(width < 40rem)"); return ( onProject(repo.id)} onAddRepo={connected ? onOpen : undefined} >

{NAMES[kind]}

); } function StateIcon({ item }: { item: Pick }) { const Icon = ICONS[item.kind][item.state]; const color = COLORS[item.kind][item.state]; return ; } function Notice({ error, loading, empty, onRetry }: { error?: string; loading?: boolean; empty: string; onRetry: () => void; }) { return (
{loading && !error && }

{error || (loading ? "Loading from GitHub…" : empty)}

{error && }
); } function Refresh({ loading, onClick }: { loading: boolean; onClick: () => void }) { return ( ); } function Labels({ labels }: Pick) { return labels.map((label) => ( {label.name} )); } function Items({ items, busy, onChannel }: { items: GithubItem[]; busy: boolean; onChannel?: (item: GithubItem) => void; }) { const id = useId(); return (
    {items.map((item) => (
  • {item.title} #{item.number} {item.state} by {item.author} · Updated {DATE.format(new Date(item.updated))} {item.labels.length > 0 && ( )} {/* Opacity keeps the actions in the tab order; devices without hover always show them. */} {onChannel && ( )} Open on GitHub
  • ))}
); } function Results({ kind, repo, data, filter, busy, onFilter, onRetry, onChannel }: { kind: GithubKind; repo: Query; data: Query; filter: Filter; busy: boolean; onFilter: (filter: Filter) => void; onRetry: () => void; onChannel?: (item: GithubItem) => void; }) { const error = repo.error || data.error; const loading = repo.loading || data.loading; if (!repo.value) { return ( ); } if (!data.value?.items.length) { const state = filter.state === "all" ? "" : `${filter.state} `; const scope = filter.search ? " match your search" : " in this project"; return ( ); } return ( <> {error &&

{error}

} {data.value.more && filter.limit < 1000 && (
)} {data.value.items.length === 1000 && (

Showing the first 1,000 results. Search to narrow the list.

)} ); } function prompt(item: GithubItem): string { const work = item.kind === "prs" ? "Continue this pull request on its existing branch: make the needed changes, validate them, and push them to it instead of opening a new pull request." : "Make the needed changes, validate them, and open or update a pull request."; return `Let's work on ${item.title} (${item.url}). Read the item and its discussion and inspect the project. ${work}`; } export function Github({ kind, project, connected, onCreate }: { kind: GithubKind; project: AppProject; connected: boolean; onCreate?: (text: string) => Promise; }) { const [filter, setFilter] = useGithubFilter(kind, project); const [search, setSearch] = useState(filter.search); const [busy, setBusy] = useState(false); const left = useLayoutLeft(); const repo = useGithub({ op: "project-repo", project: project.path, host: project.host, }); const list = useGithub( repo.value ? { op: "github-list", repo: repo.value, kind, ...filter } : undefined, ); const loading = repo.loading || list.loading; function refresh() { repo.refresh(); list.refresh(); } async function start(item: GithubItem) { if (!onCreate || busy) return; setBusy(true); try { await onCreate(prompt(item)); } finally { setBusy(false); } } return (

{NAMES[kind]}

{repo.value}
{ event.preventDefault(); setFilter({ ...filter, search: search.trim(), limit: 50 }); }} > setSearch(event.target.value)} placeholder={`Search ${NAMES[kind].toLowerCase()}…`} aria-label={`Search ${NAMES[kind].toLowerCase()}`} />
void start(item))} />
); }