diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 1b43eb4c1..0fc1065c4 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -4,11 +4,9 @@ import { FolderIcon, GitPullRequestIcon, PlusIcon, - RocketIcon, SettingsIcon, SquarePenIcon, TerminalIcon, - TriangleAlertIcon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState, type MouseEvent } from "react"; import { @@ -28,7 +26,6 @@ import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifi import { CSS } from "@dnd-kit/utilities"; import { DEFAULT_MODEL_BY_PROVIDER, - type DesktopUpdateState, ProjectId, ThreadId, type GitStatusResult, @@ -50,19 +47,6 @@ import { useComposerDraftStore } from "../composerDraftStore"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { toastManager } from "./ui/toast"; -import { - getArm64IntelBuildWarningDescription, - getDesktopUpdateActionError, - getDesktopUpdateButtonTooltip, - isDesktopUpdateButtonDisabled, - resolveDesktopUpdateButtonAction, - shouldShowArm64IntelBuildWarning, - shouldHighlightDesktopUpdateError, - shouldShowDesktopUpdateButton, - shouldToastDesktopUpdateActionResult, -} from "./desktopUpdate.logic"; -import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert"; -import { Button } from "./ui/button"; import { Collapsible, CollapsibleContent } from "./ui/collapsible"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { @@ -89,6 +73,7 @@ import { resolveThreadStatusPill, shouldClearThreadSelectionOnMouseDown, } from "./Sidebar.logic"; +import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; const EMPTY_KEYBINDINGS: ResolvedKeybindingsConfig = []; @@ -298,7 +283,6 @@ export default function Sidebar() { const renamingInputRef = useRef(null); const dragInProgressRef = useRef(false); const suppressProjectClickAfterDragRef = useRef(false); - const [desktopUpdateState, setDesktopUpdateState] = useState(null); const selectedThreadIds = useThreadSelectionStore((s) => s.selectedThreadIds); const toggleThreadSelection = useThreadSelectionStore((s) => s.toggleThread); const rangeSelectTo = useThreadSelectionStore((s) => s.rangeSelectTo); @@ -963,66 +947,6 @@ export default function Sidebar() { }; }, [clearSelection, selectedThreadIds.size]); - useEffect(() => { - if (!isElectron) return; - const bridge = window.desktopBridge; - if ( - !bridge || - typeof bridge.getUpdateState !== "function" || - typeof bridge.onUpdateState !== "function" - ) { - return; - } - - let disposed = false; - let receivedSubscriptionUpdate = false; - const unsubscribe = bridge.onUpdateState((nextState) => { - if (disposed) return; - receivedSubscriptionUpdate = true; - setDesktopUpdateState(nextState); - }); - - void bridge - .getUpdateState() - .then((nextState) => { - if (disposed || receivedSubscriptionUpdate) return; - setDesktopUpdateState(nextState); - }) - .catch(() => undefined); - - return () => { - disposed = true; - unsubscribe(); - }; - }, []); - - const showDesktopUpdateButton = isElectron && shouldShowDesktopUpdateButton(desktopUpdateState); - - const desktopUpdateTooltip = desktopUpdateState - ? getDesktopUpdateButtonTooltip(desktopUpdateState) - : "Update available"; - - const desktopUpdateButtonDisabled = isDesktopUpdateButtonDisabled(desktopUpdateState); - const desktopUpdateButtonAction = desktopUpdateState - ? resolveDesktopUpdateButtonAction(desktopUpdateState) - : "none"; - const showArm64IntelBuildWarning = - isElectron && shouldShowArm64IntelBuildWarning(desktopUpdateState); - const arm64IntelBuildWarningDescription = - desktopUpdateState && showArm64IntelBuildWarning - ? getArm64IntelBuildWarningDescription(desktopUpdateState) - : null; - const desktopUpdateButtonInteractivityClasses = desktopUpdateButtonDisabled - ? "cursor-not-allowed opacity-60" - : "hover:bg-accent hover:text-foreground"; - const desktopUpdateButtonClasses = - desktopUpdateState?.status === "downloaded" - ? "text-emerald-500" - : desktopUpdateState?.status === "downloading" - ? "text-sky-400" - : shouldHighlightDesktopUpdateError(desktopUpdateState) - ? "text-rose-500 animate-pulse" - : "text-amber-500 animate-pulse"; const newThreadShortcutLabel = useMemo( () => shortcutLabelForCommand(keybindings, "chat.newLocal") ?? @@ -1030,64 +954,6 @@ export default function Sidebar() { [keybindings], ); - const handleDesktopUpdateButtonClick = useCallback(() => { - const bridge = window.desktopBridge; - if (!bridge || !desktopUpdateState) return; - if (desktopUpdateButtonDisabled || desktopUpdateButtonAction === "none") return; - - if (desktopUpdateButtonAction === "download") { - void bridge - .downloadUpdate() - .then((result) => { - if (result.completed) { - toastManager.add({ - type: "success", - title: "Update downloaded", - description: "Restart the app from the update button to install it.", - }); - } - if (!shouldToastDesktopUpdateActionResult(result)) return; - const actionError = getDesktopUpdateActionError(result); - if (!actionError) return; - toastManager.add({ - type: "error", - title: "Could not download update", - description: actionError, - }); - }) - .catch((error) => { - toastManager.add({ - type: "error", - title: "Could not start update download", - description: error instanceof Error ? error.message : "An unexpected error occurred.", - }); - }); - return; - } - - if (desktopUpdateButtonAction === "install") { - void bridge - .installUpdate() - .then((result) => { - if (!shouldToastDesktopUpdateActionResult(result)) return; - const actionError = getDesktopUpdateActionError(result); - if (!actionError) return; - toastManager.add({ - type: "error", - title: "Could not install update", - description: actionError, - }); - }) - .catch((error) => { - toastManager.add({ - type: "error", - title: "Could not install update", - description: error instanceof Error ? error.message : "An unexpected error occurred.", - }); - }); - } - }, [desktopUpdateButtonAction, desktopUpdateButtonDisabled, desktopUpdateState]); - const expandThreadListForProject = useCallback((projectId: ProjectId) => { setExpandedThreadListsByProject((current) => { if (current.has(projectId)) return current; @@ -1136,25 +1002,6 @@ export default function Sidebar() { <> {wordmark} - {showDesktopUpdateButton && ( - - - - - } - /> - {desktopUpdateTooltip} - - )} ) : ( @@ -1164,29 +1011,6 @@ export default function Sidebar() { )} - {showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? ( - - - - Intel build on Apple Silicon - {arm64IntelBuildWarningDescription} - {desktopUpdateButtonAction !== "none" ? ( - - - - ) : null} - - - ) : null}
@@ -1610,6 +1434,7 @@ export default function Sidebar() { + {isOnSettings ? ( diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx new file mode 100644 index 000000000..3b78f1b06 --- /dev/null +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -0,0 +1,199 @@ +import { DownloadIcon, RotateCwIcon, TriangleAlertIcon, XIcon } from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; +import type { DesktopUpdateState } from "@t3tools/contracts"; +import { isElectron } from "../../env"; +import { toastManager } from "../ui/toast"; +import { + getArm64IntelBuildWarningDescription, + getDesktopUpdateActionError, + getDesktopUpdateButtonTooltip, + isDesktopUpdateButtonDisabled, + resolveDesktopUpdateButtonAction, + shouldShowArm64IntelBuildWarning, + shouldShowDesktopUpdateButton, + shouldToastDesktopUpdateActionResult, +} from "../desktopUpdate.logic"; +import { Alert, AlertDescription, AlertTitle } from "../ui/alert"; +import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; + +export function SidebarUpdatePill() { + const [state, setState] = useState(null); + const [dismissed, setDismissed] = useState(false); + + useEffect(() => { + if (!isElectron) return; + const bridge = window.desktopBridge; + if ( + !bridge || + typeof bridge.getUpdateState !== "function" || + typeof bridge.onUpdateState !== "function" + ) { + return; + } + + let disposed = false; + let receivedSubscriptionUpdate = false; + const unsubscribe = bridge.onUpdateState((nextState) => { + if (disposed) return; + receivedSubscriptionUpdate = true; + setState(nextState); + }); + + void bridge + .getUpdateState() + .then((nextState) => { + if (disposed || receivedSubscriptionUpdate) return; + setState(nextState); + }) + .catch(() => undefined); + + return () => { + disposed = true; + unsubscribe(); + }; + }, []); + + const visible = isElectron && shouldShowDesktopUpdateButton(state) && !dismissed; + const tooltip = state ? getDesktopUpdateButtonTooltip(state) : "Update available"; + const disabled = isDesktopUpdateButtonDisabled(state); + const action = state ? resolveDesktopUpdateButtonAction(state) : "none"; + + const showArm64Warning = isElectron && shouldShowArm64IntelBuildWarning(state); + const arm64Description = + state && showArm64Warning ? getArm64IntelBuildWarningDescription(state) : null; + + const handleAction = useCallback(() => { + const bridge = window.desktopBridge; + if (!bridge || !state) return; + if (disabled || action === "none") return; + + if (action === "download") { + void bridge + .downloadUpdate() + .then((result) => { + if (result.completed) { + toastManager.add({ + type: "success", + title: "Update downloaded", + description: "Restart the app from the update button to install it.", + }); + } + if (!shouldToastDesktopUpdateActionResult(result)) return; + const actionError = getDesktopUpdateActionError(result); + if (!actionError) return; + toastManager.add({ + type: "error", + title: "Could not download update", + description: actionError, + }); + }) + .catch((error) => { + toastManager.add({ + type: "error", + title: "Could not start update download", + description: error instanceof Error ? error.message : "An unexpected error occurred.", + }); + }); + return; + } + + if (action === "install") { + void bridge + .installUpdate() + .then((result) => { + if (!shouldToastDesktopUpdateActionResult(result)) return; + const actionError = getDesktopUpdateActionError(result); + if (!actionError) return; + toastManager.add({ + type: "error", + title: "Could not install update", + description: actionError, + }); + }) + .catch((error) => { + toastManager.add({ + type: "error", + title: "Could not install update", + description: error instanceof Error ? error.message : "An unexpected error occurred.", + }); + }); + } + }, [action, disabled, state]); + + if (!visible && !showArm64Warning) return null; + + return ( +
+ {showArm64Warning && arm64Description && ( + + + Intel build on Apple Silicon + {arm64Description} + + )} + {visible && ( +
+
+ + + {action === "install" ? ( + <> + + Restart to update + + ) : state?.status === "downloading" ? ( + <> + + + Downloading + {typeof state.downloadPercent === "number" + ? ` (${Math.floor(state.downloadPercent)}%)` + : "…"} + + + ) : ( + <> + + Update available + + )} + + } + /> + {tooltip} + + {action === "download" && ( + + setDismissed(true)} + > + + + } + /> + Dismiss until next launch + + )} +
+ )} +
+ ); +}