diff --git a/desktop/src/features/channels/ui/TaskChannelWorkspace.tsx b/desktop/src/features/channels/ui/TaskChannelWorkspace.tsx index 90cf23a4f9..10f1203cb5 100644 --- a/desktop/src/features/channels/ui/TaskChannelWorkspace.tsx +++ b/desktop/src/features/channels/ui/TaskChannelWorkspace.tsx @@ -1,352 +1,60 @@ -import { useQuery } from "@tanstack/react-query"; -import { GitBranch, GitPullRequest, MessagesSquare } from "lucide-react"; -import * as React from "react"; import { openUrl } from "@tauri-apps/plugin-opener"; +import * as React from "react"; import { toast } from "sonner"; - import { useCanvasQuery } from "@/features/channels/hooks"; import { githubRepositoryUrl, parseChannelBackedTask, type ChannelBackedTask, } from "@/features/channels/lib/channelBackedTask"; -import { getProjectRepoDiff } from "@/shared/api/projectGit"; -import { channelChrome } from "@/shared/layout/chromeLayout"; -import { Button } from "@/shared/ui/button"; -import { BuzzLoadingState } from "@/shared/ui/BuzzLoadingState"; +import { useTaskBranchStatus } from "@/features/channels/useTaskBranchStatus"; import { cn } from "@/shared/lib/cn"; +import { channelChrome } from "@/shared/layout/chromeLayout"; import { Markdown } from "@/shared/ui/markdown"; -import { invokeTauri } from "@/shared/api/tauri"; -type GithubReview = { - number: number; - title: string; - url: string; - state: string; - isDraft: boolean; - reviewDecision: string; - statusCheckRollup: - | { - name?: string; - context?: string; - detailsUrl?: string; - targetUrl?: string; - conclusion?: string; - status?: string; - state?: string; - }[] - | null; -}; -type GithubChanges = { - html_url: string; - files: { - filename: string; - additions: number; - deletions: number; - patch?: string; - blob_url: string; - }[]; -}; - -function useGithubTask(task: ChannelBackedTask, view: "changes" | "review") { - const repository = task.branch && githubRepositoryUrl(task.branch.repository); - return useQuery({ - queryKey: ["task-github", repository, task.branch?.name, view], - enabled: Boolean(repository && task.branch), - queryFn: () => - invokeTauri("get_task_github", { - repository: repository?.replace("https://github.com/", ""), - branch: task.branch?.name, - view, - }), - staleTime: 30_000, - retry: false, - }); -} - -type TaskView = "overview" | "changes" | "review" | "conversation"; - -const VIEWS: readonly TaskView[] = [ - "overview", - "changes", - "review", - "conversation", -]; - -function TaskLink({ href, children }: { href: string; children: string }) { - return ( - { - event.preventDefault(); - void openUrl(href).catch(() => - toast.error("Could not open link in browser"), - ); - }} - > - {children} - - ); -} - -function TaskOverview({ task }: { task: ChannelBackedTask }) { - const repository = task.branch && githubRepositoryUrl(task.branch.repository); - return ( -
-
-

- Task -

-

- {task.task.title} -

-

- {task.task.description} -

-
-
-
-
Origin
-
- -
-
-
-
Parent
-
- -
-
-
-
- Implementation branch -
-
- {repository && task.branch ? ( - - {task.branch.name} - - ) : ( - (task.branch?.name ?? "Not started") - )} -
-
-
-
- ); -} - -function TaskChanges({ task }: { task: ChannelBackedTask }) { - const branch = task.branch; - const github = useGithubTask(task, "changes"); - const isGithub = Boolean(branch && githubRepositoryUrl(branch.repository)); - const diff = useQuery({ - enabled: Boolean(branch) && !isGithub, - queryKey: ["channel-backed-task", branch?.repository, branch?.name, "diff"], - queryFn: () => - getProjectRepoDiff({ - cloneUrl: branch?.repository ?? "", - defaultBranch: branch?.name, - baseBranch: "main", - }), - retry: 1, - staleTime: 30_000, - }); - if (isGithub) { - if (github.isPending) - return ; - if (github.error) - return ( - - ); - const changes = github.data as GithubChanges; - return ( -
- - {branch?.name ?? "Branch changes"} - - {changes.files.map((file) => ( -
- - {file.filename}{" "} - +{file.additions}{" "} - −{file.deletions} - - {file.patch ? ( -
-                {file.patch}
-              
- ) : ( - View file on GitHub - )} -
- ))} -

- GitHub returns patches for up to 300 files. The branch link opens the - complete comparison. -

-
- ); - } - if (!branch) - return ( - - ); - if (diff.isLoading) - return ; - if (diff.error || !diff.data) - return ( - - ); - return ( -
-
- - {branch.name} - against main - - +{diff.data.additions}{" "} - −{diff.data.deletions} - -
-
- {diff.data.files.map((file) => ( -
- - {file.path} - - +{file.additions} - −{file.deletions} -
- ))} -
-
- ); -} - -function TaskReview({ task }: { task: ChannelBackedTask }) { - const github = useGithubTask(task, "review"); - const repository = task.branch - ? githubRepositoryUrl(task.branch.repository) - : null; - const reviewsUrl = - repository && task.branch - ? `${repository}/pulls?q=${encodeURIComponent(`is:pr head:${task.branch.name}`)}` - : null; - if (repository && task.branch) { - if (github.isPending) - return ; - if (github.error) - return ( - - ); - const prs = github.data as GithubReview[]; - if (!prs.length) - return ( - - ); - const pr = prs.find((pr) => pr.state === "OPEN") ?? prs[0]; - return ( -
-

- {`#${pr.number} ${pr.title}`} -

-

- {pr.isDraft ? "Draft" : pr.state} ·{" "} - {pr.reviewDecision || "No review decision"} -

-
    - {pr.statusCheckRollup?.map((check) => ( -
  • - {check.name ?? check.context}:{" "} - {check.conclusion || check.state || check.status} -
  • - ))} -
-
- ); - } - return ( - - - Open GitHub reviews - - - ) : undefined - } - description="Review data is not linked to this channel yet. This view is where checks, findings, and decisions will appear." - icon={} - title="No linked review" - /> - ); -} - -function EmptyView({ - action, - description, - icon, - title, +function BranchStatus({ + branch, }: { - action?: React.ReactNode; - description: string; - icon?: React.ReactNode; - title: string; + branch: NonNullable; }) { + const repository = githubRepositoryUrl(branch.repository); + const { query, pr, Icon, label: status, color } = useTaskBranchStatus(branch); + const href = + pr?.url ?? + (repository + ? `${repository}/tree/${encodeURIComponent(branch.name)}` + : null); + const label = pr ? `#${pr.number} ${pr.title}` : branch.name; return ( -
-
- {icon ? ( -
- {icon} -
- ) : null} -

{title}

-

{description}

- {action} +
+ + {query.isFetching && Checking for PR…} + {query.error && ( +

+ Could not check PR +

+ )}
); } @@ -359,38 +67,33 @@ export function TaskChannelWorkspace({ children: React.ReactNode; }) { const canvas = useCanvasQuery(channelId, channelId !== null); - const task = React.useMemo( - () => parseChannelBackedTask(canvas.data?.content), - [canvas.data?.content], + const task = parseChannelBackedTask(canvas.data?.content); + const [view, setView] = React.useState<"overview" | "conversation">( + "overview", ); - const [view, setView] = React.useState("overview"); if (!task) return children; - return (