Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions apps/web/src/components/DesktopUpdateTooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { DesktopUpdateState } from "@t3tools/contracts";
import type { ComponentProps } from "react";
import { ExternalLinkIcon } from "lucide-react";

import {
getDesktopUpdateLatestVersion,
getDesktopUpdateReleaseNotesUrl,
} from "./desktopUpdate.logic";
import { readLocalApi } from "../localApi";
import { stackedThreadToast, toastManager } from "./ui/toast";
import { TooltipPopup } from "./ui/tooltip";

type DesktopUpdateTooltipPopupProps = {
state: DesktopUpdateState | null;
summary: string;
side?: ComponentProps<typeof TooltipPopup>["side"];
};

function getDesktopUpdateTooltipTitle(state: DesktopUpdateState | null): string {
if (!state) return "Update Available";
if (state.status === "downloaded") return "Restart to Update";
if (state.status === "downloading") return "Downloading Update";
if (state.status === "error") return "Update Needs Attention";
if (state.status === "available") return "Update Available";
return "Application Update";
}

function openReleaseNotes(state: DesktopUpdateState | null) {
const api = readLocalApi();
const url = getDesktopUpdateReleaseNotesUrl(state);
void api?.shell.openExternal(url).catch((error: unknown) => {
toastManager.add(
stackedThreadToast({
type: "error",
title: "Could not open release notes",
description: error instanceof Error ? error.message : "Unable to open GitHub Releases.",
}),
);
});
}

export function DesktopUpdateTooltipPopup({
state,
summary,
side = "top",
}: DesktopUpdateTooltipPopupProps) {
const latestVersion = state ? getDesktopUpdateLatestVersion(state) : null;

return (
<TooltipPopup side={side} className="w-72 text-left">
<div className="flex flex-col gap-3 py-1">
<div className="flex flex-col gap-1">
<div className="text-sm font-semibold text-popover-foreground">
{getDesktopUpdateTooltipTitle(state)}
</div>
<div className="text-xs leading-snug text-muted-foreground">{summary}</div>
</div>
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-xs">
<span className="text-muted-foreground">Current Version</span>
<span className="min-w-0 truncate font-mono text-popover-foreground">
{state?.currentVersion ?? "Unknown"}
</span>
<span className="text-muted-foreground">Latest Version</span>
<span className="min-w-0 truncate font-mono text-popover-foreground">
{latestVersion ?? "Unknown"}
</span>
</div>
<button
type="button"
className="inline-flex h-8 w-fit items-center gap-1.5 rounded-md border border-border bg-background px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
onClick={() => openReleaseNotes(state)}
>
Release Notes
<ExternalLinkIcon className="size-3" />
</button>
</div>
</TooltipPopup>
);
}
46 changes: 46 additions & 0 deletions apps/web/src/components/desktopUpdate.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
getDesktopUpdateActionError,
getDesktopUpdateButtonTooltip,
getDesktopUpdateInstallConfirmationMessage,
getDesktopUpdateLatestVersion,
getDesktopUpdateReleaseNotesUrl,
isDesktopUpdateButtonDisabled,
resolveDesktopUpdateButtonAction,
shouldShowArm64IntelBuildWarning,
Expand Down Expand Up @@ -290,3 +292,47 @@ describe("getDesktopUpdateButtonTooltip", () => {
);
});
});

describe("desktop update release notes helpers", () => {
it("resolves the latest version from downloaded or available state", () => {
expect(
getDesktopUpdateLatestVersion({
...baseState,
availableVersion: "1.1.0",
downloadedVersion: "1.1.1",
}),
).toBe("1.1.1");
expect(getDesktopUpdateLatestVersion({ ...baseState, availableVersion: "1.1.0" })).toBe(
"1.1.0",
);
});

it("links stable updates to their GitHub release tag", () => {
expect(
getDesktopUpdateReleaseNotesUrl({
...baseState,
channel: "latest",
availableVersion: "1.1.0",
}),
).toBe("https://github.com/pingdotgg/t3code/releases/tag/v1.1.0");
});

it("links nightly updates to their GitHub release tag", () => {
expect(
getDesktopUpdateReleaseNotesUrl({
...baseState,
channel: "nightly",
availableVersion: "1.1.0-nightly.20260501.17",
}),
).toBe("https://github.com/pingdotgg/t3code/releases/tag/nightly-v1.1.0-nightly.20260501.17");
});

it("falls back to the releases page when no latest version is known", () => {
expect(getDesktopUpdateReleaseNotesUrl(null)).toBe(
"https://github.com/pingdotgg/t3code/releases",
);
expect(getDesktopUpdateReleaseNotesUrl(baseState)).toBe(
"https://github.com/pingdotgg/t3code/releases",
);
});
});
16 changes: 16 additions & 0 deletions apps/web/src/components/desktopUpdate.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/con

export type DesktopUpdateButtonAction = "download" | "install" | "none";

const DESKTOP_UPDATE_RELEASES_URL = "https://github.com/pingdotgg/t3code/releases";

export function resolveDesktopUpdateButtonAction(
state: DesktopUpdateState,
): DesktopUpdateButtonAction {
Expand Down Expand Up @@ -76,6 +78,20 @@ export function getDesktopUpdateButtonTooltip(state: DesktopUpdateState): string
return "Up to date";
}

export function getDesktopUpdateLatestVersion(state: DesktopUpdateState): string | null {
return state.downloadedVersion ?? state.availableVersion;
}

export function getDesktopUpdateReleaseNotesUrl(state: DesktopUpdateState | null): string {
const latestVersion = state ? getDesktopUpdateLatestVersion(state) : null;
if (!state || !latestVersion) {
return DESKTOP_UPDATE_RELEASES_URL;
}

const tag = state.channel === "nightly" ? `nightly-v${latestVersion}` : `v${latestVersion}`;
return `${DESKTOP_UPDATE_RELEASES_URL}/tag/${encodeURIComponent(tag)}`;
}

export function getDesktopUpdateInstallConfirmationMessage(
state: Pick<DesktopUpdateState, "availableVersion" | "downloadedVersion">,
): string {
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isDesktopUpdateButtonDisabled,
resolveDesktopUpdateButtonAction,
} from "../../components/desktopUpdate.logic";
import { DesktopUpdateTooltipPopup } from "../../components/DesktopUpdateTooltip";
import { ProviderModelPicker } from "../chat/ProviderModelPicker";
import { TraitsPicker } from "../chat/TraitsPicker";
import { resolveAndPersistPreferredEditor } from "../../editorPreferences";
Expand Down Expand Up @@ -304,7 +305,9 @@ function AboutVersionSection() {
</Button>
}
/>
{buttonTooltip ? <TooltipPopup>{buttonTooltip}</TooltipPopup> : null}
{buttonTooltip ? (
<DesktopUpdateTooltipPopup state={updateState} summary={buttonTooltip} />
) : null}
</Tooltip>
}
/>
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/sidebar/SidebarUpdatePill.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
shouldToastDesktopUpdateActionResult,
} from "../desktopUpdate.logic";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { DesktopUpdateTooltipPopup } from "../DesktopUpdateTooltip";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";

export function SidebarUpdatePill() {
Expand Down Expand Up @@ -158,7 +159,7 @@ export function SidebarUpdatePill() {
</button>
}
/>
<TooltipPopup side="top">{tooltip}</TooltipPopup>
<DesktopUpdateTooltipPopup state={state} summary={tooltip} side="top" />
</Tooltip>
{action === "download" && (
<Tooltip>
Expand Down
Loading