Skip to content

Commit ffdab91

Browse files
fix(update): soften transient update check error UX (#139)
Use a neutral, retryable footer state for transient update check failures while keeping explicit failure messaging for download/install errors. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e80f8a4 commit ffdab91

3 files changed

Lines changed: 53 additions & 12 deletions

File tree

src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function App() {
8080
const maxPanelHeightPxRef = useRef<number | null>(null)
8181
const [appVersion, setAppVersion] = useState("...")
8282

83-
const { updateStatus, triggerInstall } = useAppUpdate()
83+
const { updateStatus, triggerInstall, checkForUpdates } = useAppUpdate()
8484
const [showAbout, setShowAbout] = useState(false)
8585

8686
const trayRef = useRef<TrayIcon | null>(null)
@@ -879,6 +879,7 @@ function App() {
879879
autoUpdateNextAt={autoUpdateNextAt}
880880
updateStatus={updateStatus}
881881
onUpdateInstall={triggerInstall}
882+
onUpdateCheck={checkForUpdates}
882883
showAbout={showAbout}
883884
onShowAbout={() => setShowAbout(true)}
884885
onCloseAbout={() => setShowAbout(false)}

src/components/panel-footer.test.tsx

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ vi.mock("@tauri-apps/plugin-opener", () => ({
1111

1212
const idle: UpdateStatus = { status: "idle" }
1313
const noop = () => {}
14-
const aboutProps = { showAbout: false, onShowAbout: noop, onCloseAbout: noop }
14+
const footerProps = { showAbout: false, onShowAbout: noop, onCloseAbout: noop, onUpdateCheck: noop }
1515

1616
describe("PanelFooter", () => {
1717
it("shows countdown in minutes when >= 60 seconds", () => {
@@ -22,7 +22,7 @@ describe("PanelFooter", () => {
2222
autoUpdateNextAt={futureTime}
2323
updateStatus={idle}
2424
onUpdateInstall={noop}
25-
{...aboutProps}
25+
{...footerProps}
2626
/>
2727
)
2828
expect(screen.getByText("Next update in 5m")).toBeTruthy()
@@ -36,7 +36,7 @@ describe("PanelFooter", () => {
3636
autoUpdateNextAt={futureTime}
3737
updateStatus={idle}
3838
onUpdateInstall={noop}
39-
{...aboutProps}
39+
{...footerProps}
4040
/>
4141
)
4242
expect(screen.getByText("Next update in 30s")).toBeTruthy()
@@ -49,7 +49,7 @@ describe("PanelFooter", () => {
4949
autoUpdateNextAt={null}
5050
updateStatus={idle}
5151
onUpdateInstall={noop}
52-
{...aboutProps}
52+
{...footerProps}
5353
/>
5454
)
5555
expect(screen.getByText("Paused")).toBeTruthy()
@@ -62,7 +62,7 @@ describe("PanelFooter", () => {
6262
autoUpdateNextAt={null}
6363
updateStatus={{ status: "downloading", progress: 42 }}
6464
onUpdateInstall={noop}
65-
{...aboutProps}
65+
{...footerProps}
6666
/>
6767
)
6868
expect(screen.getByText("Downloading update 42%")).toBeTruthy()
@@ -75,7 +75,7 @@ describe("PanelFooter", () => {
7575
autoUpdateNextAt={null}
7676
updateStatus={{ status: "downloading", progress: -1 }}
7777
onUpdateInstall={noop}
78-
{...aboutProps}
78+
{...footerProps}
7979
/>
8080
)
8181
expect(screen.getByText("Downloading update...")).toBeTruthy()
@@ -89,7 +89,7 @@ describe("PanelFooter", () => {
8989
autoUpdateNextAt={null}
9090
updateStatus={{ status: "ready" }}
9191
onUpdateInstall={onInstall}
92-
{...aboutProps}
92+
{...footerProps}
9393
/>
9494
)
9595
const button = screen.getByText("Restart to update")
@@ -98,17 +98,39 @@ describe("PanelFooter", () => {
9898
expect(onInstall).toHaveBeenCalledTimes(1)
9999
})
100100

101-
it("shows error state", () => {
101+
it("shows retryable updates soon state for update check failures", async () => {
102+
const onUpdateCheck = vi.fn()
103+
render(
104+
<PanelFooter
105+
version="0.0.0"
106+
autoUpdateNextAt={null}
107+
updateStatus={{ status: "error", message: "Update check failed" }}
108+
onUpdateInstall={noop}
109+
showAbout={false}
110+
onShowAbout={noop}
111+
onCloseAbout={noop}
112+
onUpdateCheck={onUpdateCheck}
113+
/>
114+
)
115+
116+
const retryButton = screen.getByRole("button", { name: "Updates soon" })
117+
expect(retryButton).toBeTruthy()
118+
await userEvent.click(retryButton)
119+
expect(onUpdateCheck).toHaveBeenCalledTimes(1)
120+
})
121+
122+
it("shows error state for non-check failures", () => {
102123
const { container } = render(
103124
<PanelFooter
104125
version="0.0.0"
105126
autoUpdateNextAt={null}
106-
updateStatus={{ status: "error", message: "oops" }}
127+
updateStatus={{ status: "error", message: "Download failed" }}
107128
onUpdateInstall={noop}
108-
{...aboutProps}
129+
{...footerProps}
109130
/>
110131
)
111132
expect(container.textContent).toContain("Update failed")
133+
expect(screen.queryByRole("button", { name: "Updates soon" })).toBeNull()
112134
})
113135

114136
it("shows installing state", () => {
@@ -118,7 +140,7 @@ describe("PanelFooter", () => {
118140
autoUpdateNextAt={null}
119141
updateStatus={{ status: "installing" }}
120142
onUpdateInstall={noop}
121-
{...aboutProps}
143+
{...footerProps}
122144
/>
123145
)
124146
expect(screen.getByText("Installing...")).toBeTruthy()
@@ -136,6 +158,7 @@ describe("PanelFooter", () => {
136158
showAbout={showAbout}
137159
onShowAbout={() => setShowAbout(true)}
138160
onCloseAbout={() => setShowAbout(false)}
161+
onUpdateCheck={noop}
139162
/>
140163
)
141164
}

src/components/panel-footer.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface PanelFooterProps {
99
autoUpdateNextAt: number | null;
1010
updateStatus: UpdateStatus;
1111
onUpdateInstall: () => void;
12+
onUpdateCheck: () => void;
1213
showAbout: boolean;
1314
onShowAbout: () => void;
1415
onCloseAbout: () => void;
@@ -18,11 +19,13 @@ function VersionDisplay({
1819
version,
1920
updateStatus,
2021
onUpdateInstall,
22+
onUpdateCheck,
2123
onVersionClick,
2224
}: {
2325
version: string;
2426
updateStatus: UpdateStatus;
2527
onUpdateInstall: () => void;
28+
onUpdateCheck: () => void;
2629
onVersionClick: () => void;
2730
}) {
2831
switch (updateStatus.status) {
@@ -50,6 +53,18 @@ function VersionDisplay({
5053
<span className="text-xs text-muted-foreground">Installing...</span>
5154
);
5255
case "error":
56+
if (updateStatus.message === "Update check failed") {
57+
return (
58+
<button
59+
type="button"
60+
onClick={onUpdateCheck}
61+
className="text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
62+
title={updateStatus.message}
63+
>
64+
Updates soon
65+
</button>
66+
);
67+
}
5368
return (
5469
<span className="text-xs text-destructive" title={updateStatus.message}>
5570
Update failed
@@ -73,6 +88,7 @@ export function PanelFooter({
7388
autoUpdateNextAt,
7489
updateStatus,
7590
onUpdateInstall,
91+
onUpdateCheck,
7692
showAbout,
7793
onShowAbout,
7894
onCloseAbout,
@@ -100,6 +116,7 @@ export function PanelFooter({
100116
version={version}
101117
updateStatus={updateStatus}
102118
onUpdateInstall={onUpdateInstall}
119+
onUpdateCheck={onUpdateCheck}
103120
onVersionClick={onShowAbout}
104121
/>
105122
<span className="text-xs text-muted-foreground tabular-nums">

0 commit comments

Comments
 (0)