Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dad415c
feat(web): move update button to sidebar footer as dismissable pill
Noojuno Mar 15, 2026
5f41c53
refactor(web): extract SidebarUpdatePill into self-contained component
Noojuno Mar 15, 2026
161d78f
feat(web): restore arm64 warning Alert UI in update pill
Noojuno Mar 15, 2026
05a9165
chore(web): remove test mocks from SidebarUpdatePill
Noojuno Mar 15, 2026
b0b60be
feat(desktop): add manual check-for-updates from web settings UI
AriajSarkar Mar 15, 2026
40c1777
refactor: use queryOptions pattern with real-time sync and multi-purp…
AriajSarkar Mar 16, 2026
17de2fc
feat(web): add tooltip to update button in settings
AriajSarkar Mar 17, 2026
1051bb8
fix(settings): remove redundant getCheckForUpdateButtonLabel, use act…
AriajSarkar Mar 24, 2026
8541519
fix: tooltip fallback returns 'Up to date' for idle/up-to-date states…
AriajSarkar Mar 24, 2026
f11e84a
feat(web): add about settings section for updates
shivamhwp Mar 28, 2026
f2b79c9
refine settings about layout
shivamhwp Mar 28, 2026
89b546f
Merge remote-tracking branch 'upstream/main' into update-overhaul
shivamhwp Mar 28, 2026
0bfc742
chore(web): clean up sidebar imports after merge
shivamhwp Mar 28, 2026
cf33c2c
Update About card description for available updates
shivamhwp Mar 28, 2026
513aef2
Fix about update card install action
shivamhwp Mar 28, 2026
c01b791
Fix sidebar context clicks and update status handling
shivamhwp Mar 28, 2026
1dd3783
Merge remote-tracking branch 'upstream/main' into update-overhaul
shivamhwp Mar 29, 2026
b38aa2b
chore(web): clean up sidebar after main merge
shivamhwp Mar 29, 2026
0febe9b
Merge About settings into General and redirect old route
shivamhwp Mar 29, 2026
480a7ff
Refine desktop update state caching
shivamhwp Mar 29, 2026
35c3d02
Remove desktop update error highlighting helper
shivamhwp Mar 29, 2026
1e6d5f2
Merge origin/main into update-overhaul
juliusmarminge Mar 29, 2026
524135c
Refine desktop update UI and install flow
juliusmarminge Mar 29, 2026
868f6c5
rm redirect route
juliusmarminge Mar 29, 2026
b98abaf
rm hideHeader
juliusmarminge Mar 29, 2026
2928da6
archive
juliusmarminge Mar 29, 2026
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
25 changes: 22 additions & 3 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as Effect from "effect/Effect";
import type {
DesktopTheme,
DesktopUpdateActionResult,
DesktopUpdateCheckResult,
DesktopUpdateState,
} from "@t3tools/contracts";
import { autoUpdater } from "electron-updater";
Expand Down Expand Up @@ -56,6 +57,7 @@ const UPDATE_STATE_CHANNEL = "desktop:update-state";
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
const UPDATE_CHECK_CHANNEL = "desktop:update-check";
const GET_WS_URL_CHANNEL = "desktop:get-ws-url";
const BASE_DIR = process.env.T3CODE_HOME?.trim() || Path.join(OS.homedir(), ".t3");
const STATE_DIR = Path.join(BASE_DIR, "userdata");
Expand Down Expand Up @@ -756,26 +758,28 @@ function shouldEnableAutoUpdates(): boolean {
);
}

async function checkForUpdates(reason: string): Promise<void> {
if (isQuitting || !updaterConfigured || updateCheckInFlight) return;
async function checkForUpdates(reason: string): Promise<boolean> {
if (isQuitting || !updaterConfigured || updateCheckInFlight) return false;
if (updateState.status === "downloading" || updateState.status === "downloaded") {
console.info(
`[desktop-updater] Skipping update check (${reason}) while status=${updateState.status}.`,
);
return;
return false;
}
updateCheckInFlight = true;
setUpdateState(reduceDesktopUpdateStateOnCheckStart(updateState, new Date().toISOString()));
console.info(`[desktop-updater] Checking for updates (${reason})...`);

try {
await autoUpdater.checkForUpdates();
return true;
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
setUpdateState(
reduceDesktopUpdateStateOnCheckFailure(updateState, message, new Date().toISOString()),
);
console.error(`[desktop-updater] Failed to check for updates: ${message}`);
return true;
} finally {
updateCheckInFlight = false;
}
Expand Down Expand Up @@ -1263,6 +1267,21 @@ function registerIpcHandlers(): void {
state: updateState,
} satisfies DesktopUpdateActionResult;
});

ipcMain.removeHandler(UPDATE_CHECK_CHANNEL);
ipcMain.handle(UPDATE_CHECK_CHANNEL, async () => {
if (!updaterConfigured) {
return {
checked: false,
state: updateState,
} satisfies DesktopUpdateCheckResult;
}
const checked = await checkForUpdates("web-ui");
return {
checked,
state: updateState,
} satisfies DesktopUpdateCheckResult;
});
}

function getIconOption(): { icon: string } | Record<string, never> {
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
const MENU_ACTION_CHANNEL = "desktop:menu-action";
const UPDATE_STATE_CHANNEL = "desktop:update-state";
const UPDATE_GET_STATE_CHANNEL = "desktop:update-get-state";
const UPDATE_CHECK_CHANNEL = "desktop:update-check";
const UPDATE_DOWNLOAD_CHANNEL = "desktop:update-download";
const UPDATE_INSTALL_CHANNEL = "desktop:update-install";
const GET_WS_URL_CHANNEL = "desktop:get-ws-url";
Expand All @@ -35,6 +36,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
};
},
getUpdateState: () => ipcRenderer.invoke(UPDATE_GET_STATE_CHANNEL),
checkForUpdate: () => ipcRenderer.invoke(UPDATE_CHECK_CHANNEL),
downloadUpdate: () => ipcRenderer.invoke(UPDATE_DOWNLOAD_CHANNEL),
installUpdate: () => ipcRenderer.invoke(UPDATE_INSTALL_CHANNEL),
onUpdateState: (listener) => {
Expand Down
188 changes: 8 additions & 180 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import {
FolderIcon,
GitPullRequestIcon,
PlusIcon,
RocketIcon,
SettingsIcon,
SquarePenIcon,
TerminalIcon,
TriangleAlertIcon,
} from "lucide-react";
import { ProjectFavicon } from "./ProjectFavicon";
import { autoAnimate } from "@formkit/auto-animate";
Expand Down Expand Up @@ -39,7 +37,6 @@ import { restrictToFirstScrollableAncestor, restrictToVerticalAxis } from "@dnd-
import { CSS } from "@dnd-kit/utilities";
import {
DEFAULT_MODEL_BY_PROVIDER,
type DesktopUpdateState,
ProjectId,
ThreadId,
type GitStatusResult,
Expand Down Expand Up @@ -67,19 +64,6 @@ import { selectThreadTerminalState, useTerminalStateStore } from "../terminalSta
import { toastManager } from "./ui/toast";
import { formatRelativeTimeLabel } from "../timestampFormat";
import { SettingsSidebarNav } from "./settings/SettingsSidebarNav";
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 { Menu, MenuGroup, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
import {
Expand Down Expand Up @@ -110,6 +94,7 @@ import {
sortProjectsForSidebar,
sortThreadsForSidebar,
} from "./Sidebar.logic";
import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill";
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
import { useSettings, useUpdateSettings } from "~/hooks/useSettings";

Expand Down Expand Up @@ -358,7 +343,6 @@ export default function Sidebar() {
const dragInProgressRef = useRef(false);
const suppressProjectClickAfterDragRef = useRef(false);
const suppressProjectClickForContextMenuRef = useRef(false);
const [desktopUpdateState, setDesktopUpdateState] = useState<DesktopUpdateState | null>(null);
const selectedThreadIds = useThreadSelectionStore((s) => s.selectedThreadIds);
const toggleThreadSelection = useThreadSelectionStore((s) => s.toggleThread);
const rangeSelectTo = useThreadSelectionStore((s) => s.rangeSelectTo);
Expand Down Expand Up @@ -1429,127 +1413,12 @@ 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 =
shortcutLabelForCommand(keybindings, "chat.newLocal") ??
shortcutLabelForCommand(keybindings, "chat.new");

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 newThreadShortcutLabel = useMemo(
() =>
shortcutLabelForCommand(keybindings, "chat.newLocal") ??
shortcutLabelForCommand(keybindings, "chat.new"),
[keybindings],
);

const expandThreadListForProject = useCallback((projectId: ProjectId) => {
setExpandedThreadListsByProject((current) => {
Expand Down Expand Up @@ -1599,25 +1468,6 @@ export default function Sidebar() {
<>
<SidebarHeader className="drag-region h-[52px] flex-row items-center gap-2 px-4 py-0 pl-[90px]">
{wordmark}
{showDesktopUpdateButton && (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
aria-label={desktopUpdateTooltip}
aria-disabled={desktopUpdateButtonDisabled || undefined}
disabled={desktopUpdateButtonDisabled}
className={`inline-flex size-7 ml-auto mt-1.5 items-center justify-center rounded-md text-muted-foreground transition-colors ${desktopUpdateButtonInteractivityClasses} ${desktopUpdateButtonClasses}`}
onClick={handleDesktopUpdateButtonClick}
>
<RocketIcon className="size-3.5" />
</button>
}
/>
<TooltipPopup side="bottom">{desktopUpdateTooltip}</TooltipPopup>
</Tooltip>
)}
</SidebarHeader>
</>
) : (
Expand All @@ -1631,29 +1481,6 @@ export default function Sidebar() {
) : (
<>
<SidebarContent className="gap-0">
{showArm64IntelBuildWarning && arm64IntelBuildWarningDescription ? (
<SidebarGroup className="px-2 pt-2 pb-0">
<Alert variant="warning" className="rounded-2xl border-warning/40 bg-warning/8">
<TriangleAlertIcon />
<AlertTitle>Intel build on Apple Silicon</AlertTitle>
<AlertDescription>{arm64IntelBuildWarningDescription}</AlertDescription>
{desktopUpdateButtonAction !== "none" ? (
<AlertAction>
<Button
size="xs"
variant="outline"
disabled={desktopUpdateButtonDisabled}
onClick={handleDesktopUpdateButtonClick}
>
{desktopUpdateButtonAction === "download"
? "Download ARM build"
: "Install ARM build"}
</Button>
</AlertAction>
) : null}
</Alert>
</SidebarGroup>
) : null}
<SidebarGroup className="px-2 py-2">
<div className="mb-1 flex items-center justify-between pl-2 pr-1.5">
<span className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60">
Expand Down Expand Up @@ -1804,6 +1631,7 @@ export default function Sidebar() {

<SidebarSeparator />
<SidebarFooter className="p-2">
<SidebarUpdatePill />
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
Expand Down
Loading
Loading