|
| 1 | +import { Cloud, Spinner } from "@phosphor-icons/react"; |
| 2 | +import { Button as QuillButton } from "@posthog/quill"; |
| 3 | +import type { Task } from "@posthog/shared/domain-types"; |
| 4 | +import { useAuthStateValue } from "@posthog/ui/features/auth/store"; |
| 5 | +import { AutoresearchHeaderButton } from "@posthog/ui/features/autoresearch/AutoresearchHeaderButton"; |
| 6 | +import { useDiffStatsToggle } from "@posthog/ui/features/code-review/hooks/useDiffStatsToggle"; |
| 7 | +import { |
| 8 | + formatHotkey, |
| 9 | + SHORTCUTS, |
| 10 | +} from "@posthog/ui/features/command/keyboard-shortcuts"; |
| 11 | +import { DiffStatsBadge } from "@posthog/ui/features/diff-stats/DiffStatsBadge"; |
| 12 | +import { useFeatureFlag } from "@posthog/ui/features/feature-flags/useFeatureFlag"; |
| 13 | +import { BranchSelector } from "@posthog/ui/features/git-interaction/components/BranchSelector"; |
| 14 | +import { CloudGitInteractionHeader } from "@posthog/ui/features/git-interaction/components/CloudGitInteractionHeader"; |
| 15 | +import { TaskActionsMenu } from "@posthog/ui/features/git-interaction/components/TaskActionsMenu"; |
| 16 | +import { HandoffConfirmDialog } from "@posthog/ui/features/sessions/components/HandoffConfirmDialog"; |
| 17 | +import { useHandoffDialogStore } from "@posthog/ui/features/sessions/handoffDialogStore"; |
| 18 | +import { useSessionCallbacks } from "@posthog/ui/features/sessions/hooks/useSessionCallbacks"; |
| 19 | +import { useSessionForTask } from "@posthog/ui/features/sessions/useSession"; |
| 20 | +import { SkillButtonsMenu } from "@posthog/ui/features/skill-buttons/components/SkillButtonsMenu"; |
| 21 | +import { useWorkspace } from "@posthog/ui/features/workspace/useWorkspace"; |
| 22 | +import { Tooltip } from "@posthog/ui/primitives/Tooltip"; |
| 23 | +import { Flex } from "@radix-ui/themes"; |
| 24 | +import { useState } from "react"; |
| 25 | + |
| 26 | +const CLOUD_HANDOFF_FLAG = "phc-cloud-handoff"; |
| 27 | + |
| 28 | +function LocalHandoffButton({ taskId, task }: { taskId: string; task: Task }) { |
| 29 | + const session = useSessionForTask(taskId); |
| 30 | + const workspace = useWorkspace(taskId); |
| 31 | + const repoPath = workspace?.folderPath ?? null; |
| 32 | + const authStatus = useAuthStateValue((s) => s.status); |
| 33 | + const cloudHandoffEnabled = |
| 34 | + useFeatureFlag(CLOUD_HANDOFF_FLAG) || import.meta.env.DEV; |
| 35 | + const { initiateHandoffToCloud } = useSessionCallbacks({ |
| 36 | + taskId, |
| 37 | + task, |
| 38 | + session: session ?? undefined, |
| 39 | + repoPath, |
| 40 | + }); |
| 41 | + |
| 42 | + const confirmOpen = useHandoffDialogStore((s) => s.confirmOpen); |
| 43 | + const direction = useHandoffDialogStore((s) => s.direction); |
| 44 | + const branchName = useHandoffDialogStore((s) => s.branchName); |
| 45 | + const openConfirm = useHandoffDialogStore((s) => s.openConfirm); |
| 46 | + const closeConfirm = useHandoffDialogStore((s) => s.closeConfirm); |
| 47 | + |
| 48 | + const [isSubmitting, setIsSubmitting] = useState(false); |
| 49 | + const [error, setError] = useState<string | null>(null); |
| 50 | + |
| 51 | + if (authStatus !== "authenticated") return null; |
| 52 | + if (!cloudHandoffEnabled) return null; |
| 53 | + |
| 54 | + const handleConfirm = async () => { |
| 55 | + setError(null); |
| 56 | + setIsSubmitting(true); |
| 57 | + try { |
| 58 | + await initiateHandoffToCloud(); |
| 59 | + } catch (err) { |
| 60 | + setError(err instanceof Error ? err.message : "Handoff failed"); |
| 61 | + } finally { |
| 62 | + setIsSubmitting(false); |
| 63 | + } |
| 64 | + }; |
| 65 | + |
| 66 | + const inProgress = session?.handoffInProgress ?? false; |
| 67 | + |
| 68 | + return ( |
| 69 | + <> |
| 70 | + <div className="no-drag flex items-center"> |
| 71 | + <QuillButton |
| 72 | + variant="outline" |
| 73 | + size="sm" |
| 74 | + disabled={inProgress} |
| 75 | + onClick={() => |
| 76 | + openConfirm(taskId, "to-cloud", workspace?.branchName ?? null) |
| 77 | + } |
| 78 | + > |
| 79 | + {inProgress ? ( |
| 80 | + <Spinner size={14} className="shrink-0 animate-spin" /> |
| 81 | + ) : ( |
| 82 | + <Cloud size={14} weight="regular" className="shrink-0" /> |
| 83 | + )} |
| 84 | + {inProgress ? "Transferring..." : "Continue in cloud"} |
| 85 | + </QuillButton> |
| 86 | + </div> |
| 87 | + {confirmOpen && direction === "to-cloud" && ( |
| 88 | + <HandoffConfirmDialog |
| 89 | + open={confirmOpen} |
| 90 | + onOpenChange={(open) => { |
| 91 | + if (!open) { |
| 92 | + closeConfirm(); |
| 93 | + setError(null); |
| 94 | + } |
| 95 | + }} |
| 96 | + direction="to-cloud" |
| 97 | + branchName={branchName} |
| 98 | + onConfirm={handleConfirm} |
| 99 | + isSubmitting={isSubmitting} |
| 100 | + error={error} |
| 101 | + /> |
| 102 | + )} |
| 103 | + </> |
| 104 | + ); |
| 105 | +} |
| 106 | + |
| 107 | +function TaskDiffStatsBadge({ task }: { task: Task }) { |
| 108 | + const { filesChanged, linesAdded, linesRemoved, isOpen, toggle } = |
| 109 | + useDiffStatsToggle(task, "split"); |
| 110 | + return ( |
| 111 | + <Tooltip |
| 112 | + content={isOpen ? "Close review panel" : "Open review panel"} |
| 113 | + shortcut={formatHotkey(SHORTCUTS.TOGGLE_REVIEW_PANEL)} |
| 114 | + side="bottom" |
| 115 | + > |
| 116 | + <DiffStatsBadge |
| 117 | + filesChanged={filesChanged} |
| 118 | + linesAdded={linesAdded} |
| 119 | + linesRemoved={linesRemoved} |
| 120 | + active={isOpen} |
| 121 | + onClick={toggle} |
| 122 | + /> |
| 123 | + </Tooltip> |
| 124 | + ); |
| 125 | +} |
| 126 | + |
| 127 | +// A task detail's header action row: skill buttons, autoresearch, branch |
| 128 | +// selector, review-panel toggle (diff stats), cloud/local handoff, and the |
| 129 | +// task actions menu. Shared by the code-space ContentHeader and the |
| 130 | +// channels-space title bar (WebsiteLayout), which renders its own header. |
| 131 | +export function TaskHeaderActions({ task }: { task: Task }) { |
| 132 | + const workspace = useWorkspace(task.id); |
| 133 | + const isCloudTask = workspace?.mode === "cloud"; |
| 134 | + |
| 135 | + return ( |
| 136 | + <Flex |
| 137 | + align="center" |
| 138 | + justify="end" |
| 139 | + gap="1" |
| 140 | + pr="1" |
| 141 | + pl="1" |
| 142 | + className="h-full max-w-[50%] shrink-0 overflow-hidden" |
| 143 | + > |
| 144 | + <div className="no-drag"> |
| 145 | + <SkillButtonsMenu taskId={task.id} /> |
| 146 | + </div> |
| 147 | + <div className="no-drag"> |
| 148 | + <AutoresearchHeaderButton taskId={task.id} /> |
| 149 | + </div> |
| 150 | + {workspace && (workspace.branchName || workspace.baseBranch) && ( |
| 151 | + <div className="no-drag flex h-full min-w-0 items-center"> |
| 152 | + <BranchSelector |
| 153 | + repoPath={workspace.worktreePath ?? workspace.folderPath ?? null} |
| 154 | + currentBranch={workspace.branchName ?? workspace.baseBranch ?? null} |
| 155 | + taskId={task.id} |
| 156 | + /> |
| 157 | + </div> |
| 158 | + )} |
| 159 | + <TaskDiffStatsBadge task={task} /> |
| 160 | + |
| 161 | + {isCloudTask ? ( |
| 162 | + <CloudGitInteractionHeader taskId={task.id} task={task} /> |
| 163 | + ) : ( |
| 164 | + <LocalHandoffButton taskId={task.id} task={task} /> |
| 165 | + )} |
| 166 | + <TaskActionsMenu taskId={task.id} isCloud={isCloudTask} /> |
| 167 | + </Flex> |
| 168 | + ); |
| 169 | +} |
0 commit comments