From 3e3c4037cdf173df94e21b7fe00058fcea7e19a6 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Thu, 23 Jul 2026 21:27:06 +0530 Subject: [PATCH 01/11] docs: spec for update progress modal Phase-narrated, dismissible-with-corner-pill update install UX to replace the fire-and-forget toast path. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...2026-07-23-update-progress-modal-design.md | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-23-update-progress-modal-design.md diff --git a/docs/superpowers/specs/2026-07-23-update-progress-modal-design.md b/docs/superpowers/specs/2026-07-23-update-progress-modal-design.md new file mode 100644 index 00000000..27710ff6 --- /dev/null +++ b/docs/superpowers/specs/2026-07-23-update-progress-modal-design.md @@ -0,0 +1,162 @@ +# Update Progress Modal — Design + +**Date:** 2026-07-23 +**Scope:** `apps/desktop-ui` (Tauri desktop UI). No Rust changes. + +## Problem + +When a new signed build is available the app surfaces a persistent toast +(`desktop-init.tsx`). Clicking **Restart & update** calls `installUpdate()` +with **no progress callback** — the toast flips to a static "Downloading…" +line, then 10–60s later the app relaunches with no further signal. To the user +this reads as a frozen / buggy app: no motion, no percentage, then a sudden +disappearance and restart. + +The settings "Check for updates" dialog *does* draw a progress bar, but it +stops at the download percentage and then goes silent through install + +relaunch. The About-card check button is also fire-and-forget. + +**Goal:** every path that installs an update shows a continuously-animated, +phase-narrated progress UI so the user always knows the app is working and is +about to restart. + +## Current flow (map) + +| File | Role | +|---|---| +| `src/lib/desktop/updater.ts` | `checkForUpdate()` / `installUpdate(onProgress)` / `currentAppVersion()`. Sees every Tauri `DownloadEvent` (`Started`/`Progress`/`Finished`) then `relaunch()`. | +| `src/components/desktop/desktop-init.tsx` | App-wide launch + 6h auto-check → persistent toast. **Fire-and-forget install (the bug).** Mounted via `app/layout.tsx:123`. | +| `src/components/desktop/desktop-update-dialog.tsx` | Settings "Check for updates" modal + hand-rolled download-only bar. | +| `src/components/desktop/app-version-label.tsx` | Settings About card check button. Fire-and-forget install. | +| `src/components/ui/progress.tsx` | Radix progress bar, already in repo, currently unused by updater. | + +## Design (Approach A — shared app-wide modal) + +### 1. Phase model in `updater.ts` + +Replace the `onProgress?: (p: DownloadProgress)` callback with a single status +callback that narrates the whole install: + +```ts +export type UpdatePhase = "downloading" | "installing" | "restarting"; +export type UpdateStatus = { + phase: UpdatePhase; + downloaded: number; + total: number | null; +}; + +export async function installUpdate( + onStatus?: (s: UpdateStatus) => void +): Promise +``` + +Phase transitions inside the existing `downloadAndInstall` callback: + +- `Started` → capture `total`, emit `downloading` (downloaded 0). +- `Progress` → accumulate, emit `downloading`. +- `Finished` → emit `installing` (Tauri is now swapping the bundle in place). +- After `downloadAndInstall` resolves → emit `restarting`, then `relaunch()`. + +`total` may be `null` (server sends no `Content-Length`) → the UI shows an +indeterminate bar, never a fabricated percentage. + +### 2. `useUpdateInstall()` hook (new: `src/lib/desktop/use-update-install.ts`) + +Owns install state for the whole app. Returns: + +```ts +{ + status: UpdateStatus | null, // null = idle / not installing + error: string | null, + start: () => void, // idempotent: no-op if already running + dismiss: () => void, // hides modal, keeps corner pill (see §4) + reopen: () => void, // corner pill → reopen modal + visible: boolean, // modal open? +} +``` + +`start()` calls `installUpdate(setStatus)` and catches errors into `error`. +Because success ends in `relaunch()`, there is no "done" state — the process +dies. State lives in a module-level store (tiny `useSyncExternalStore` or a +2-field zustand-free pub/sub) so the app-wide modal and any trigger share one +instance regardless of where in the tree they mount. + +### 3. `` (new: `src/components/desktop/update-progress-modal.tsx`) + +Mounted once, app-wide, next to `DesktopInit` in `app/layout.tsx`. Reads the +hook. Renders nothing when `status` is null. + +Per phase (framer-motion crossfade between phase labels): + +| Phase | Bar | Copy | +|---|---|---| +| `downloading` (total known) | determinate `` | `Downloading update… {pct}%` | +| `downloading` (total null) | indeterminate shimmer | `Downloading update…` | +| `installing` | indeterminate shimmer | `Installing update…` | +| `restarting` | indeterminate shimmer | `Restarting MyDevTools…` | +| error | none | error text + **Retry** / **Close** | + +- All copy through `useTranslations("DesktopUpdate")` (new namespace) — no + hardcoded strings (project rule). +- Dialog enter animation via framer-motion; indeterminate bar is a CSS + keyframe shimmer (no JS). +- **Dismissible while `downloading`/`installing`** (close X + backdrop) per + user choice. **`restarting` forces the modal open** (no dismiss) — the + restart is imminent, so the user must see it; dismissing a 0.5s phase would + just flash. +- Header line: "Version X is available" style + reassurance ("Your offline + data is never touched."). + +### 4. Corner return pill (new, in the same modal file or a sibling) + +When the user dismisses the modal mid-install, a small fixed bottom-right pill +stays: spinner + `Updating… {pct}%`, clickable to `reopen()`. Visible whenever +`status !== null && !visible`. Uses the same hook. This is the "dismissible + +corner return" flow. + +### 5. Wire the three triggers to the hook + +- **`desktop-init.tsx`** — toast action `onClick` → `start()` (via the shared + store's imperative `startUpdate()` export, since the toast callback isn't a + React child of the provider). Drop the fire-and-forget `installUpdate()` + + manual `toast.loading`. Toast dismisses; modal takes over. +- **`desktop-update-dialog.tsx`** — replace the hand-rolled bar; call + `start()` and let the app-wide modal render progress. Dialog just triggers + and closes. (Removes `pct()` + inline bar markup.) +- **`app-version-label.tsx`** — check button, on update-found → `start()`. + +### Error handling + +- `installUpdate` throws (download/verify/IO fail) → hook sets `error`, modal + shows Retry/Close. Retry re-runs `start()`. Close clears state. +- Signature verification failure surfaces as the thrown error message (already + the case in the Tauri plugin) — shown verbatim, no silent swallow. + +## What we are NOT building (YAGNI) + +- No full-screen takeover overlay (user chose contained modal). +- No Rust-side changes — the plugin already emits everything needed. +- No zustand store / global state lib — one tiny module-level pub/sub is + enough for a single ephemeral install. +- No "update history" / changelog viewer beyond the existing `notes` line. +- No background silent-install — install stays user-initiated (existing + product behavior; relaunch is never sprung silently). + +## Testing + +- Unit: `installUpdate` phase sequencing — feed a fake `Update` whose + `downloadAndInstall` invokes the callback with `Started`→`Progress`×N→ + `Finished`, assert emitted phases are `downloading…`, then `installing`, + then `restarting`, and `relaunch` called last. (`updater` mockable via the + dynamic-import boundary.) +- Unit: `pct` / percentage helper — `total: null` → indeterminate (no number); + clamps at 100. +- Manual: real update against the release endpoint — verify each phase label + renders, dismiss→pill→reopen works, error path shows Retry. + +## Files + +**New:** `use-update-install.ts`, `update-progress-modal.tsx`, `DesktopUpdate` +i18n namespace (en + 26 locales). +**Edit:** `updater.ts`, `desktop-init.tsx`, `desktop-update-dialog.tsx`, +`app-version-label.tsx`, `app/layout.tsx`. From 06f71befbecfc6169ced437a0fec07920bf1bf81 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Thu, 23 Jul 2026 21:54:20 +0530 Subject: [PATCH 02/11] docs: implementation plan for update progress modal Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-07-23-update-progress-modal.md | 744 ++++++++++++++++++ 1 file changed, 744 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-23-update-progress-modal.md diff --git a/docs/superpowers/plans/2026-07-23-update-progress-modal.md b/docs/superpowers/plans/2026-07-23-update-progress-modal.md new file mode 100644 index 00000000..6a5a45f4 --- /dev/null +++ b/docs/superpowers/plans/2026-07-23-update-progress-modal.md @@ -0,0 +1,744 @@ +# Update Progress Modal Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the fire-and-forget update-install paths with one app-wide, phase-narrated, animated progress modal so the user always sees the app downloading, installing, and restarting instead of a silent freeze-then-relaunch. + +**Architecture:** `installUpdate()` gains a phase-aware status callback (downloading → installing → restarting) mapped from the Tauri `DownloadEvent`s it already receives. A module-level install store (`use-update-install.ts`) owns install state and exposes both an imperative `startUpdate()` (for the sonner toast callback, which lives outside the React tree) and a `useUpdateInstall()` hook. A single `` mounted app-wide in `layout.tsx` renders progress; dismissing it leaves a corner pill that reopens it. All three triggers (auto-check toast, settings dialog, About card) route through the store. + +**Tech Stack:** Next.js (static export for Tauri), React, TypeScript, `@tauri-apps/plugin-updater`, `@tauri-apps/plugin-process`, sonner, framer-motion (installed), Radix `ui/progress.tsx`, next-intl, jest. + +## Global Constraints + +- Work only in `apps/desktop-ui`. No Rust / `src-tauri` changes — the plugin already emits every event needed. +- Build/verify with repo-local bins: `./node_modules/.bin/tsc --noEmit`, `./node_modules/.bin/jest`. `pnpm exec` is broken. +- All new user-visible modal/pill strings via `useTranslations("DesktopUpdate")` — no hardcoded UI text in the new components. Existing sibling updater strings (toast/dialog/About English text) are NOT migrated (out of scope). +- New `DesktopUpdate` namespace added to `messages/en.json` **and all 26 other locales**. A missing key falls back to English via the `deepMerge` in `src/i18n/request.ts`, so en is load-bearing; the 26 locales get translated copies. +- Desktop-only: every trigger and the modal gate on `isDesktop()` (or render null on web). +- Edit files with Read/Edit tools, not shell `sed`/`cat`. +- Commit after each task. + +--- + +### Task 1: Phase-aware status in `updater.ts` + +**Files:** +- Modify: `apps/desktop-ui/src/lib/desktop/updater.ts` +- Test: `apps/desktop-ui/src/lib/__tests__/updater-status.test.ts` + +**Interfaces:** +- Consumes: `DownloadEvent`, `Update` from `@tauri-apps/plugin-updater`. +- Produces: + - `type UpdatePhase = "downloading" | "installing" | "restarting"` + - `type UpdateStatus = { phase: UpdatePhase; downloaded: number; total: number | null }` + - `function reduceDownloadEvent(prev: { downloaded: number; total: number | null }, event: DownloadEvent): UpdateStatus` (pure, exported for tests) + - `function pctOf(s: { downloaded: number; total: number | null }): number | null` (clamped 0–100, null when total falsy) + - `installUpdate(onStatus?: (s: UpdateStatus) => void): Promise` (callback signature CHANGED from `DownloadProgress` to `UpdateStatus`) + +- [ ] **Step 1: Write the failing test** + +Create `apps/desktop-ui/src/lib/__tests__/updater-status.test.ts`: + +```ts +import { reduceDownloadEvent, pctOf } from "@/lib/desktop/updater"; + +describe("reduceDownloadEvent", () => { + it("Started captures total and resets downloaded", () => { + const s = reduceDownloadEvent( + { downloaded: 999, total: null }, + { event: "Started", data: { contentLength: 500 } } as any + ); + expect(s).toEqual({ phase: "downloading", downloaded: 0, total: 500 }); + }); + + it("Started with no contentLength → total null (indeterminate)", () => { + const s = reduceDownloadEvent( + { downloaded: 0, total: null }, + { event: "Started", data: {} } as any + ); + expect(s.total).toBeNull(); + expect(s.phase).toBe("downloading"); + }); + + it("Progress accumulates chunk lengths, keeps total", () => { + const s = reduceDownloadEvent( + { downloaded: 100, total: 500 }, + { event: "Progress", data: { chunkLength: 50 } } as any + ); + expect(s).toEqual({ phase: "downloading", downloaded: 150, total: 500 }); + }); + + it("Finished switches phase to installing", () => { + const s = reduceDownloadEvent( + { downloaded: 500, total: 500 }, + { event: "Finished", data: {} } as any + ); + expect(s.phase).toBe("installing"); + }); +}); + +describe("pctOf", () => { + it("null total → null (indeterminate, no fake number)", () => { + expect(pctOf({ downloaded: 10, total: null })).toBeNull(); + }); + it("rounds and clamps at 100", () => { + expect(pctOf({ downloaded: 250, total: 500 })).toBe(50); + expect(pctOf({ downloaded: 600, total: 500 })).toBe(100); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `./node_modules/.bin/jest src/lib/__tests__/updater-status.test.ts` +Expected: FAIL — `reduceDownloadEvent`/`pctOf` not exported. + +- [ ] **Step 3: Implement in `updater.ts`** + +Replace the `DownloadProgress` type (line 24) and the `installUpdate` body (lines 48-77). Keep `checkForUpdate`, `currentAppVersion`, `pending` unchanged. + +```ts +import type { DownloadEvent, Update } from "@tauri-apps/plugin-updater"; + +export type UpdatePhase = "downloading" | "installing" | "restarting"; +export type UpdateStatus = { + phase: UpdatePhase; + downloaded: number; + total: number | null; +}; + +/** Map a Tauri DownloadEvent to the next status. Pure — unit tested. */ +export function reduceDownloadEvent( + prev: { downloaded: number; total: number | null }, + event: DownloadEvent +): UpdateStatus { + switch (event.event) { + case "Started": + return { + phase: "downloading", + downloaded: 0, + total: event.data.contentLength ?? null, + }; + case "Progress": + return { + phase: "downloading", + downloaded: prev.downloaded + event.data.chunkLength, + total: prev.total, + }; + case "Finished": + return { phase: "installing", downloaded: prev.downloaded, total: prev.total }; + } +} + +/** Download percentage, or null when the server sent no Content-Length. */ +export function pctOf(s: { downloaded: number; total: number | null }): number | null { + if (!s.total) return null; + return Math.min(100, Math.round((s.downloaded / s.total) * 100)); +} +``` + +And rewrite `installUpdate`: + +```ts +export async function installUpdate( + onStatus?: (s: UpdateStatus) => void +): Promise { + if (!pending) { + await checkForUpdate(); + if (!pending) throw new Error("No update available to install"); + } + const update = pending; + let acc: { downloaded: number; total: number | null } = { + downloaded: 0, + total: null, + }; + + await update.downloadAndInstall((event) => { + const s = reduceDownloadEvent(acc, event); + acc = { downloaded: s.downloaded, total: s.total }; + onStatus?.(s); + }); + + // Bundle swapped in place; now relaunch into the new version. + onStatus?.({ phase: "restarting", downloaded: acc.downloaded, total: acc.total }); + const { relaunch } = await import("@tauri-apps/plugin-process"); + await relaunch(); +} +``` + +Remove the old `DownloadProgress` export (its only consumers are updated in Tasks 4–5). + +- [ ] **Step 4: Run test to verify it passes** + +Run: `./node_modules/.bin/jest src/lib/__tests__/updater-status.test.ts` +Expected: PASS (6 tests). + +- [ ] **Step 5: Commit** + +```bash +git add apps/desktop-ui/src/lib/desktop/updater.ts apps/desktop-ui/src/lib/__tests__/updater-status.test.ts +git commit -m "feat(updater): phase-aware install status callback" +``` + +--- + +### Task 2: Install store + `useUpdateInstall` hook + +**Files:** +- Create: `apps/desktop-ui/src/lib/desktop/use-update-install.ts` +- Test: `apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts` + +**Interfaces:** +- Consumes: `installUpdate`, `UpdateStatus` from Task 1. +- Produces: + - `function startUpdate(): void` — imperative; idempotent (no-op while a run is active); begins install, drives status. + - `function dismissUpdate(): void` — hides modal, keeps run active (corner pill). + - `function reopenUpdate(): void` — shows modal again. + - `function retryUpdate(): void` — clears error+status, restarts. + - `function closeUpdate(): void` — clears everything (error dismissal). + - `function useUpdateInstall(): { status: UpdateStatus | null; error: string | null; visible: boolean; dismiss: () => void; reopen: () => void; retry: () => void; close: () => void }` + +- [ ] **Step 1: Write the failing test** + +Create `apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts`. Mock `installUpdate` so we control status emission without Tauri: + +```ts +let emit: ((s: any) => void) | undefined; +let resolveInstall: (() => void) | undefined; + +jest.mock("@/lib/desktop/updater", () => ({ + installUpdate: jest.fn((onStatus?: (s: any) => void) => { + emit = onStatus; + return new Promise((res) => { + resolveInstall = res; + }); + }), +})); + +import { startUpdate, dismissUpdate, reopenUpdate, closeUpdate } from "@/lib/desktop/use-update-install"; +// read module state through a tiny getter the module exposes for tests: +import { __getState } from "@/lib/desktop/use-update-install"; + +beforeEach(() => { + emit = undefined; + resolveInstall = undefined; + closeUpdate(); // reset store between tests +}); + +describe("install store", () => { + it("startUpdate opens modal and enters downloading", async () => { + startUpdate(); + expect(__getState().visible).toBe(true); + expect(__getState().status?.phase).toBe("downloading"); + }); + + it("status emissions update the store", async () => { + startUpdate(); + await Promise.resolve(); // let dynamic import().then microtask flush + emit?.({ phase: "installing", downloaded: 5, total: 5 }); + expect(__getState().status?.phase).toBe("installing"); + }); + + it("startUpdate is idempotent while running", async () => { + startUpdate(); + const first = __getState().status; + startUpdate(); + expect(__getState().status).toBe(first); + }); + + it("dismiss hides modal but keeps the run; reopen shows it", () => { + startUpdate(); + dismissUpdate(); + expect(__getState().visible).toBe(false); + expect(__getState().status).not.toBeNull(); + reopenUpdate(); + expect(__getState().visible).toBe(true); + }); + + it("a rejected install sets error", async () => { + const { installUpdate } = require("@/lib/desktop/updater"); + (installUpdate as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error("boom"))); + startUpdate(); + await Promise.resolve(); + await Promise.resolve(); + expect(__getState().error).toBe("boom"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `./node_modules/.bin/jest src/lib/__tests__/use-update-install.test.ts` +Expected: FAIL — module does not exist. + +- [ ] **Step 3: Implement `use-update-install.ts`** + +```ts +"use client"; + +import { useSyncExternalStore } from "react"; + +import type { UpdateStatus } from "@/lib/desktop/updater"; + +type State = { + status: UpdateStatus | null; + error: string | null; + visible: boolean; +}; + +let state: State = { status: null, error: null, visible: false }; +const subscribers = new Set<() => void>(); + +function set(patch: Partial): void { + state = { ...state, ...patch }; + subscribers.forEach((fn) => fn()); +} + +/** Imperative entry point — usable from the sonner toast callback (no React tree). */ +export function startUpdate(): void { + if (state.status) return; // already running + set({ + status: { phase: "downloading", downloaded: 0, total: null }, + error: null, + visible: true, + }); + void import("@/lib/desktop/updater") + .then(({ installUpdate }) => installUpdate((s) => set({ status: s }))) + .catch((e) => + set({ error: e instanceof Error ? e.message : "Update failed to install" }) + ); + // On success the process relaunches — there is no resolve branch to handle. +} + +export function dismissUpdate(): void { + set({ visible: false }); +} +export function reopenUpdate(): void { + set({ visible: true }); +} +export function retryUpdate(): void { + set({ status: null, error: null }); + startUpdate(); +} +export function closeUpdate(): void { + set({ status: null, error: null, visible: false }); +} + +/** Test-only accessor for the module store. */ +export function __getState(): State { + return state; +} + +function subscribe(cb: () => void): () => void { + subscribers.add(cb); + return () => { + subscribers.delete(cb); + }; +} + +export function useUpdateInstall() { + const snap = useSyncExternalStore( + subscribe, + () => state, + () => state + ); + return { + ...snap, + dismiss: dismissUpdate, + reopen: reopenUpdate, + retry: retryUpdate, + close: closeUpdate, + }; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `./node_modules/.bin/jest src/lib/__tests__/use-update-install.test.ts` +Expected: PASS (5 tests). + +- [ ] **Step 5: Commit** + +```bash +git add apps/desktop-ui/src/lib/desktop/use-update-install.ts apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts +git commit -m "feat(updater): app-wide install store + useUpdateInstall hook" +``` + +--- + +### Task 3: `` + corner pill + +**Files:** +- Create: `apps/desktop-ui/src/components/desktop/update-progress-modal.tsx` + +**Interfaces:** +- Consumes: `useUpdateInstall` (Task 2), `pctOf` (Task 1), `useTranslations("DesktopUpdate")` (Task 6 adds keys; en fallback covers it), `Dialog*` from `ui/dialog`, `Progress` from `ui/progress`, `motion`/`AnimatePresence` from `framer-motion`, lucide `Loader2`. +- Produces: `export function UpdateProgressModal(): JSX.Element | null` — self-contained; reads the store, renders nothing when idle. + +Behavior: +- Idle (`status === null`) → render null. +- `restarting` phase → modal is forced open regardless of `visible` (imminent relaunch must be seen) and is non-dismissible. +- `downloading` / `installing` while `visible` → dismissible modal (close button + backdrop call `dismiss()`). +- Not visible but run active → corner pill (fixed bottom-right) with spinner + `Updating… {pct}%`, click → `reopen()`. +- `error !== null` → modal shows error text + Retry (`retry()`) / Close (`close()`); pill hidden. +- Determinate bar via `` when `pctOf` returns a number; otherwise a framer-motion indeterminate sweep. + +- [ ] **Step 1: Implement the component** + +```tsx +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; +import { Loader2 } from "lucide-react"; +import { useTranslations } from "next-intl"; + +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Progress } from "@/components/ui/progress"; +import { pctOf, type UpdatePhase } from "@/lib/desktop/updater"; +import { useUpdateInstall } from "@/lib/desktop/use-update-install"; + +function IndeterminateBar() { + return ( +
+ +
+ ); +} + +export function UpdateProgressModal() { + const t = useTranslations("DesktopUpdate"); + const { status, error, visible, dismiss, reopen, retry, close } = useUpdateInstall(); + + if (!status) return null; + + const pct = pctOf(status); + const phase: UpdatePhase = status.phase; + const locked = phase === "restarting"; // force-open, non-dismissible + const open = locked || (visible && !error) || (!!error && visible); + + const label = + error != null + ? t("failed") + : phase === "installing" + ? t("installing") + : phase === "restarting" + ? t("restarting") + : pct !== null + ? t("downloadingPct", { pct }) + : t("downloading"); + + // Corner pill when the run is active but the modal is dismissed (no error). + if (!open && !error) { + return ( + + ); + } + + return ( + { + if (locked || error) return; // restart & errors don't backdrop-dismiss + if (!o) dismiss(); + }} + > + + + {t("title")} + + + {error != null ? ( +
+

{error}

+
+ + +
+
+ ) : ( +
+ {pct !== null && phase === "downloading" ? ( + + ) : ( + + )} + + + {label} + + +

{t("reassurance")}

+
+ )} +
+
+ ); +} +``` + +Note on `hideClose`: check `ui/dialog.tsx` for whether `DialogContent` accepts a prop to hide the close button. If it does, pass it truthy when `locked`. If it does NOT, delete the `hideClose` line — the `onOpenChange` guard already blocks dismissal during restart, and the close button clicking through is acceptable (restart is sub-second). Do not invent the prop. + +- [ ] **Step 2: Typecheck** + +Run: `./node_modules/.bin/tsc --noEmit` +Expected: no new errors from `update-progress-modal.tsx`. (Namespace keys resolve at runtime via en.json — added in Task 6; tsc does not type-check message keys.) + +- [ ] **Step 3: Commit** + +```bash +git add apps/desktop-ui/src/components/desktop/update-progress-modal.tsx +git commit -m "feat(updater): app-wide update progress modal + corner pill" +``` + +--- + +### Task 4: Mount modal app-wide + rewire the auto-check toast + +**Files:** +- Modify: `apps/desktop-ui/src/app/layout.tsx` (near line 123, where `` mounts) +- Modify: `apps/desktop-ui/src/components/desktop/desktop-init.tsx:69-80` + +**Interfaces:** +- Consumes: `UpdateProgressModal` (Task 3), `startUpdate` (Task 2). + +- [ ] **Step 1: Mount the modal in `layout.tsx`** + +Add the import and render it right after ``: + +```tsx +import { UpdateProgressModal } from "@/components/desktop/update-progress-modal"; +// ... + + +``` + +- [ ] **Step 2: Rewire the toast action in `desktop-init.tsx`** + +Replace the `onClick` (lines 69-80) so it hands off to the store instead of fire-and-forget. The toast text stays English (sibling, not migrated). Remove the `toast.loading`/`toast.error` install handling — the modal now owns progress and errors. + +```tsx +action: { + label: "Restart & update", + onClick: () => { + toast.dismiss("desktop-update"); + void import("@/lib/desktop/use-update-install").then((m) => m.startUpdate()); + }, +}, +``` + +(The `import("@/lib/desktop/updater")` at line 58 for `checkForUpdate` stays; only the install branch changes.) + +- [ ] **Step 3: Typecheck** + +Run: `./node_modules/.bin/tsc --noEmit` +Expected: no new errors. + +- [ ] **Step 4: Verify existing suites still green** + +Run: `./node_modules/.bin/jest src/lib/__tests__/updater-status.test.ts src/lib/__tests__/use-update-install.test.ts` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add apps/desktop-ui/src/app/layout.tsx apps/desktop-ui/src/components/desktop/desktop-init.tsx +git commit -m "feat(updater): route auto-check toast into the progress modal" +``` + +--- + +### Task 5: Rewire settings dialog + About card to the store + +**Files:** +- Modify: `apps/desktop-ui/src/components/desktop/desktop-update-dialog.tsx` +- Modify: `apps/desktop-ui/src/components/desktop/app-version-label.tsx:33-49` + +**Interfaces:** +- Consumes: `startUpdate` (Task 2). + +- [ ] **Step 1: `desktop-update-dialog.tsx` — delegate install to the store** + +Remove the hand-rolled bar and local progress state; the app-wide modal renders progress. Delete the `pct` helper (lines 17-20), the `installing`/`progress` state (lines 32-33), the `percent` calc (line 63), and the inline bar block (lines 97-108). Replace `install` (lines 50-61) with: + +```tsx +const install = async () => { + const { startUpdate } = await import("@/lib/desktop/use-update-install"); + startUpdate(); + setOpen(false); // hand off to the app-wide progress modal +}; +``` + +Drop the `DownloadProgress` import (line 15 → keep only `UpdateInfo`). The update card keeps its "Download & install" button (lines 109-114) which now calls `install`; remove the `installing ? ... : ...` conditional (lines 97-114) and always render the button, since progress moved out. The dialog's `onOpenChange` guard `!installing && setOpen(o)` (line 72) → just `setOpen(o)`. + +- [ ] **Step 2: `app-version-label.tsx` — delegate install** + +Replace `handleCheck` (lines 33-49) install branch: + +```tsx +const handleCheck = async () => { + setChecking(true); + try { + const { checkForUpdate } = await import("@/lib/desktop/updater"); + const update = await checkForUpdate(); + if (!update) { + toast.success("You're on the latest version"); + return; + } + const { startUpdate } = await import("@/lib/desktop/use-update-install"); + startUpdate(); + } catch { + toast.error("Couldn't check for updates"); + } finally { + setChecking(false); + } +}; +``` + +(Toast strings stay English — sibling, not migrated.) + +- [ ] **Step 3: Typecheck** + +Run: `./node_modules/.bin/tsc --noEmit` +Expected: no new errors; no dangling `DownloadProgress` / `pct` references. + +- [ ] **Step 4: Commit** + +```bash +git add apps/desktop-ui/src/components/desktop/desktop-update-dialog.tsx apps/desktop-ui/src/components/desktop/app-version-label.tsx +git commit -m "feat(updater): route settings dialog + about card into progress modal" +``` + +--- + +### Task 6: `DesktopUpdate` i18n namespace (27 locales) + +**Files:** +- Modify: `apps/desktop-ui/messages/en.json` (+ 26 others: `af, ar, ca, cs, da, de, el, es, fa, fr, id, it, ja, ko, ms, nb, nl, pl, pt, pt-BR, ru, sv, tr, uk, vi, zh`) + +**Interfaces:** +- Consumes: nothing. Produces the `DesktopUpdate` namespace consumed by Task 3. + +- [ ] **Step 1: Add the English block to `en.json`** + +Add a top-level `"DesktopUpdate"` namespace (place it alphabetically-ish near other Desktop* / Settings blocks; exact position doesn't matter): + +```json +"DesktopUpdate": { + "title": "Updating MyDevTools", + "downloading": "Downloading update…", + "downloadingPct": "Downloading update… {pct}%", + "installing": "Installing update…", + "restarting": "Restarting MyDevTools…", + "reassurance": "Your offline data is never touched.", + "failed": "Update failed to install", + "retry": "Retry", + "close": "Close", + "pill": "Updating…", + "pillPct": "Updating… {pct}%" +} +``` + +- [ ] **Step 2: Add translated blocks to the 26 locale files** + +For each locale file, add the same `DesktopUpdate` namespace with the values translated into that language. Keep the `{pct}` ICU placeholder verbatim (no space changes inside braces), keep "MyDevTools" as the untranslated product name. Example (`fr.json`): + +```json +"DesktopUpdate": { + "title": "Mise à jour de MyDevTools", + "downloading": "Téléchargement de la mise à jour…", + "downloadingPct": "Téléchargement de la mise à jour… {pct} %", + "installing": "Installation de la mise à jour…", + "restarting": "Redémarrage de MyDevTools…", + "reassurance": "Vos données hors ligne ne sont jamais touchées.", + "failed": "Échec de l'installation de la mise à jour", + "retry": "Réessayer", + "close": "Fermer", + "pill": "Mise à jour…", + "pillPct": "Mise à jour… {pct} %" +} +``` + +There are no ICU plural forms in this namespace (only a `{pct}` number substitution), so ru/uk/pl/cs/ar need no few/many/plural categories here — plain translated strings suffice. + +- [ ] **Step 3: Validate all 27 files parse and contain the namespace** + +Run: +```bash +cd apps/desktop-ui && for f in messages/*.json; do node -e "const j=require('./$f'); if(!j.DesktopUpdate||!j.DesktopUpdate.title){console.error('MISSING DesktopUpdate in $f');process.exit(1)}" || exit 1; done && echo "all 27 OK" +``` +Expected: `all 27 OK` (also proves every file is still valid JSON). + +- [ ] **Step 4: Commit** + +```bash +git add apps/desktop-ui/messages/*.json +git commit -m "i18n(updater): add DesktopUpdate namespace across 27 locales" +``` + +--- + +### Task 7: Full verification pass + +**Files:** none (verification only). + +- [ ] **Step 1: Typecheck the whole app** + +Run: `cd apps/desktop-ui && ./node_modules/.bin/tsc --noEmit` +Expected: no errors introduced by this work. + +- [ ] **Step 2: Run the new tests + confirm no regressions in touched areas** + +Run: `cd apps/desktop-ui && ./node_modules/.bin/jest src/lib/__tests__/updater-status.test.ts src/lib/__tests__/use-update-install.test.ts` +Expected: PASS. (Known pre-existing failing suites — react-window, pending-invitations-badge, encrypted-tool-placeholder, workspace-store — are unrelated; do not fix here.) + +- [ ] **Step 3: Manual smoke (documented, run in a real desktop build)** + +Against the release endpoint with a newer build available: +1. Launch → auto-check toast appears → click **Restart & update** → toast dismisses, modal opens showing a moving Downloading bar with %, then **Installing update…**, then **Restarting MyDevTools…**, then relaunch. +2. During download, close the modal → corner pill shows `Updating… N%` → click it → modal reopens. +3. Settings → Check for updates → Download & install → dialog closes, same modal takes over. +4. Force an error (offline mid-download) → modal shows the error + Retry/Close; Retry restarts. + +- [ ] **Step 4: Commit (if any doc/notes changed)** — otherwise nothing to commit. + +--- + +## Self-Review + +**Spec coverage:** +- Phase model (downloading/installing/restarting/error) → Task 1 (phases) + Task 3 (error UI). ✓ +- `useUpdateInstall` hook + imperative `startUpdate` for the toast → Task 2. ✓ +- App-wide `` mounted in layout → Tasks 3–4. ✓ +- Corner return pill / dismissible flow → Task 3. ✓ +- `restarting` forces modal open → Task 3 (`locked`). ✓ +- Indeterminate bar when `total` null, no fake percent → Task 1 `pctOf` + Task 3 `IndeterminateBar`. ✓ +- Rewire all three triggers → Tasks 4 (toast) + 5 (dialog, About). ✓ +- Reuse `ui/progress.tsx`, drop hand-rolled bar → Task 5. ✓ +- 27-locale `DesktopUpdate` namespace, siblings not migrated → Task 6. ✓ +- No Rust changes → honored throughout. ✓ + +**Placeholder scan:** No TBD/TODO. The only conditional instruction (`hideClose` in Task 3) is explicitly resolved: check the prop, use it or delete the line — not left open. Locale translations in Task 6 are content the implementer produces per-locale, with a worked `fr` example and a validation gate; the key set and English source are fully specified. + +**Type consistency:** `UpdateStatus`/`UpdatePhase`/`reduceDownloadEvent`/`pctOf` defined in Task 1, consumed by the same names in Tasks 2–3. `startUpdate`/`dismiss`/`reopen`/`retry`/`close` defined in Task 2, used by the same names in Tasks 3–5. Callback signature change (`DownloadProgress` → `UpdateStatus`) is propagated to every consumer (Tasks 4–5 remove the old references). ✓ From 83bc903ddc0d33fa81fc759074384942a6bbeb34 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Thu, 23 Jul 2026 21:58:39 +0530 Subject: [PATCH 03/11] feat(updater): phase-aware install status callback --- .../src/lib/__tests__/updater-status.test.ts | 46 +++++++++++++ apps/desktop-ui/src/lib/desktop/updater.ts | 64 +++++++++++++------ 2 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 apps/desktop-ui/src/lib/__tests__/updater-status.test.ts diff --git a/apps/desktop-ui/src/lib/__tests__/updater-status.test.ts b/apps/desktop-ui/src/lib/__tests__/updater-status.test.ts new file mode 100644 index 00000000..d285221d --- /dev/null +++ b/apps/desktop-ui/src/lib/__tests__/updater-status.test.ts @@ -0,0 +1,46 @@ +import { reduceDownloadEvent, pctOf } from "@/lib/desktop/updater"; + +describe("reduceDownloadEvent", () => { + it("Started captures total and resets downloaded", () => { + const s = reduceDownloadEvent( + { downloaded: 999, total: null }, + { event: "Started", data: { contentLength: 500 } } as any + ); + expect(s).toEqual({ phase: "downloading", downloaded: 0, total: 500 }); + }); + + it("Started with no contentLength → total null (indeterminate)", () => { + const s = reduceDownloadEvent( + { downloaded: 0, total: null }, + { event: "Started", data: {} } as any + ); + expect(s.total).toBeNull(); + expect(s.phase).toBe("downloading"); + }); + + it("Progress accumulates chunk lengths, keeps total", () => { + const s = reduceDownloadEvent( + { downloaded: 100, total: 500 }, + { event: "Progress", data: { chunkLength: 50 } } as any + ); + expect(s).toEqual({ phase: "downloading", downloaded: 150, total: 500 }); + }); + + it("Finished switches phase to installing", () => { + const s = reduceDownloadEvent( + { downloaded: 500, total: 500 }, + { event: "Finished", data: {} } as any + ); + expect(s.phase).toBe("installing"); + }); +}); + +describe("pctOf", () => { + it("null total → null (indeterminate, no fake number)", () => { + expect(pctOf({ downloaded: 10, total: null })).toBeNull(); + }); + it("rounds and clamps at 100", () => { + expect(pctOf({ downloaded: 250, total: 500 })).toBe(50); + expect(pctOf({ downloaded: 600, total: 500 })).toBe(100); + }); +}); diff --git a/apps/desktop-ui/src/lib/desktop/updater.ts b/apps/desktop-ui/src/lib/desktop/updater.ts index 5274b0e5..0b82e532 100644 --- a/apps/desktop-ui/src/lib/desktop/updater.ts +++ b/apps/desktop-ui/src/lib/desktop/updater.ts @@ -10,7 +10,7 @@ * Every update is verified against the updater public key baked into * tauri.conf.json before it's allowed to install. */ -import type { Update } from "@tauri-apps/plugin-updater"; +import type { DownloadEvent, Update } from "@tauri-apps/plugin-updater"; import { isDesktop } from "./is-desktop"; @@ -21,7 +21,41 @@ export type UpdateInfo = { date?: string; }; -export type DownloadProgress = { downloaded: number; total: number | null }; +export type UpdatePhase = "downloading" | "installing" | "restarting"; +export type UpdateStatus = { + phase: UpdatePhase; + downloaded: number; + total: number | null; +}; + +/** Map a Tauri DownloadEvent to the next status. Pure — unit tested. */ +export function reduceDownloadEvent( + prev: { downloaded: number; total: number | null }, + event: DownloadEvent +): UpdateStatus { + switch (event.event) { + case "Started": + return { + phase: "downloading", + downloaded: 0, + total: event.data.contentLength ?? null, + }; + case "Progress": + return { + phase: "downloading", + downloaded: prev.downloaded + event.data.chunkLength, + total: prev.total, + }; + case "Finished": + return { phase: "installing", downloaded: prev.downloaded, total: prev.total }; + } +} + +/** Download percentage, or null when the server sent no Content-Length. */ +export function pctOf(s: { downloaded: number; total: number | null }): number | null { + if (!s.total) return null; + return Math.min(100, Math.round((s.downloaded / s.total) * 100)); +} // Handle to the Update object between check() and install so we don't re-check. let pending: Update | null = null; @@ -46,32 +80,26 @@ export async function checkForUpdate(): Promise { * Re-checks first if nothing is pending (e.g. called directly). */ export async function installUpdate( - onProgress?: (p: DownloadProgress) => void + onStatus?: (s: UpdateStatus) => void ): Promise { if (!pending) { await checkForUpdate(); if (!pending) throw new Error("No update available to install"); } const update = pending; - let downloaded = 0; - let total: number | null = null; + let acc: { downloaded: number; total: number | null } = { + downloaded: 0, + total: null, + }; await update.downloadAndInstall((event) => { - switch (event.event) { - case "Started": - total = event.data.contentLength ?? null; - break; - case "Progress": - downloaded += event.data.chunkLength; - onProgress?.({ downloaded, total }); - break; - case "Finished": - onProgress?.({ downloaded, total }); - break; - } + const s = reduceDownloadEvent(acc, event); + acc = { downloaded: s.downloaded, total: s.total }; + onStatus?.(s); }); - // Restart into the freshly installed version. + // Bundle swapped in place; now relaunch into the new version. + onStatus?.({ phase: "restarting", downloaded: acc.downloaded, total: acc.total }); const { relaunch } = await import("@tauri-apps/plugin-process"); await relaunch(); } From f666503dfca47f4330d26b4954a3dc065f75e558 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Thu, 23 Jul 2026 22:10:43 +0530 Subject: [PATCH 04/11] feat(updater): app-wide install store + useUpdateInstall hook Co-Authored-By: Claude Opus 4.8 (1M context) --- .../lib/__tests__/use-update-install.test.ts | 61 +++++++++++++++ .../src/lib/desktop/use-update-install.ts | 76 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts create mode 100644 apps/desktop-ui/src/lib/desktop/use-update-install.ts diff --git a/apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts b/apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts new file mode 100644 index 00000000..d91c8705 --- /dev/null +++ b/apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts @@ -0,0 +1,61 @@ +let emit: ((s: any) => void) | undefined; +let resolveInstall: (() => void) | undefined; + +jest.mock("@/lib/desktop/updater", () => ({ + installUpdate: jest.fn((onStatus?: (s: any) => void) => { + emit = onStatus; + return new Promise((res) => { + resolveInstall = res; + }); + }), +})); + +import { startUpdate, dismissUpdate, reopenUpdate, closeUpdate } from "@/lib/desktop/use-update-install"; +// read module state through a tiny getter the module exposes for tests: +import { __getState } from "@/lib/desktop/use-update-install"; + +beforeEach(() => { + emit = undefined; + resolveInstall = undefined; + closeUpdate(); // reset store between tests +}); + +describe("install store", () => { + it("startUpdate opens modal and enters downloading", async () => { + startUpdate(); + expect(__getState().visible).toBe(true); + expect(__getState().status?.phase).toBe("downloading"); + }); + + it("status emissions update the store", async () => { + startUpdate(); + await Promise.resolve(); // let dynamic import().then microtask flush + emit?.({ phase: "installing", downloaded: 5, total: 5 }); + expect(__getState().status?.phase).toBe("installing"); + }); + + it("startUpdate is idempotent while running", async () => { + startUpdate(); + const first = __getState().status; + startUpdate(); + expect(__getState().status).toBe(first); + }); + + it("dismiss hides modal but keeps the run; reopen shows it", () => { + startUpdate(); + dismissUpdate(); + expect(__getState().visible).toBe(false); + expect(__getState().status).not.toBeNull(); + reopenUpdate(); + expect(__getState().visible).toBe(true); + }); + + it("a rejected install sets error", async () => { + const { installUpdate } = require("@/lib/desktop/updater"); + (installUpdate as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error("boom"))); + startUpdate(); + await Promise.resolve(); + await Promise.resolve(); + expect(__getState().error).toBe("boom"); + }); +}); diff --git a/apps/desktop-ui/src/lib/desktop/use-update-install.ts b/apps/desktop-ui/src/lib/desktop/use-update-install.ts new file mode 100644 index 00000000..01a09e83 --- /dev/null +++ b/apps/desktop-ui/src/lib/desktop/use-update-install.ts @@ -0,0 +1,76 @@ +"use client"; + +import { useSyncExternalStore } from "react"; + +import type { UpdateStatus } from "@/lib/desktop/updater"; + +type State = { + status: UpdateStatus | null; + error: string | null; + visible: boolean; +}; + +let state: State = { status: null, error: null, visible: false }; +const subscribers = new Set<() => void>(); + +function set(patch: Partial): void { + state = { ...state, ...patch }; + subscribers.forEach((fn) => fn()); +} + +/** Imperative entry point — usable from the sonner toast callback (no React tree). */ +export function startUpdate(): void { + if (state.status) return; // already running + set({ + status: { phase: "downloading", downloaded: 0, total: null }, + error: null, + visible: true, + }); + void import("@/lib/desktop/updater") + .then(({ installUpdate }) => installUpdate((s) => set({ status: s }))) + .catch((e) => + set({ error: e instanceof Error ? e.message : "Update failed to install" }) + ); + // On success the process relaunches — there is no resolve branch to handle. +} + +export function dismissUpdate(): void { + set({ visible: false }); +} +export function reopenUpdate(): void { + set({ visible: true }); +} +export function retryUpdate(): void { + set({ status: null, error: null }); + startUpdate(); +} +export function closeUpdate(): void { + set({ status: null, error: null, visible: false }); +} + +/** Test-only accessor for the module store. */ +export function __getState(): State { + return state; +} + +function subscribe(cb: () => void): () => void { + subscribers.add(cb); + return () => { + subscribers.delete(cb); + }; +} + +export function useUpdateInstall() { + const snap = useSyncExternalStore( + subscribe, + () => state, + () => state + ); + return { + ...snap, + dismiss: dismissUpdate, + reopen: reopenUpdate, + retry: retryUpdate, + close: closeUpdate, + }; +} From e3734ff2595573749e9457dd005236db831db102 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Thu, 23 Jul 2026 22:13:20 +0530 Subject: [PATCH 05/11] test(updater): fix jest mock hoisting in install-store test --- .../lib/__tests__/use-update-install.test.ts | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts b/apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts index d91c8705..0959748a 100644 --- a/apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts +++ b/apps/desktop-ui/src/lib/__tests__/use-update-install.test.ts @@ -1,27 +1,32 @@ -let emit: ((s: any) => void) | undefined; -let resolveInstall: (() => void) | undefined; +import { installUpdate } from "@/lib/desktop/updater"; jest.mock("@/lib/desktop/updater", () => ({ - installUpdate: jest.fn((onStatus?: (s: any) => void) => { - emit = onStatus; - return new Promise((res) => { - resolveInstall = res; - }); - }), + installUpdate: jest.fn(), })); -import { startUpdate, dismissUpdate, reopenUpdate, closeUpdate } from "@/lib/desktop/use-update-install"; -// read module state through a tiny getter the module exposes for tests: -import { __getState } from "@/lib/desktop/use-update-install"; +import { + startUpdate, + dismissUpdate, + reopenUpdate, + closeUpdate, + __getState, +} from "@/lib/desktop/use-update-install"; + +const mockInstall = installUpdate as jest.Mock; +let emit: ((s: any) => void) | undefined; beforeEach(() => { emit = undefined; - resolveInstall = undefined; - closeUpdate(); // reset store between tests + closeUpdate(); // reset the module store between tests + mockInstall.mockReset(); + mockInstall.mockImplementation((onStatus?: (s: any) => void) => { + emit = onStatus; + return new Promise(() => {}); // capture callback, never resolve + }); }); describe("install store", () => { - it("startUpdate opens modal and enters downloading", async () => { + it("startUpdate opens modal and enters downloading", () => { startUpdate(); expect(__getState().visible).toBe(true); expect(__getState().status?.phase).toBe("downloading"); @@ -29,12 +34,12 @@ describe("install store", () => { it("status emissions update the store", async () => { startUpdate(); - await Promise.resolve(); // let dynamic import().then microtask flush + await new Promise((r) => setTimeout(r, 0)); // let dynamic import().then flush emit?.({ phase: "installing", downloaded: 5, total: 5 }); expect(__getState().status?.phase).toBe("installing"); }); - it("startUpdate is idempotent while running", async () => { + it("startUpdate is idempotent while running", () => { startUpdate(); const first = __getState().status; startUpdate(); @@ -51,11 +56,9 @@ describe("install store", () => { }); it("a rejected install sets error", async () => { - const { installUpdate } = require("@/lib/desktop/updater"); - (installUpdate as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error("boom"))); + mockInstall.mockImplementationOnce(() => Promise.reject(new Error("boom"))); startUpdate(); - await Promise.resolve(); - await Promise.resolve(); + await new Promise((r) => setTimeout(r, 0)); expect(__getState().error).toBe("boom"); }); }); From ccdcb3c4fa4b1d06e1f30954b39da44f50fc305f Mon Sep 17 00:00:00 2001 From: AKHIL Date: Thu, 23 Jul 2026 22:18:17 +0530 Subject: [PATCH 06/11] feat(updater): app-wide update progress modal + corner pill --- .../desktop/update-progress-modal.tsx | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 apps/desktop-ui/src/components/desktop/update-progress-modal.tsx diff --git a/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx b/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx new file mode 100644 index 00000000..56c5dac9 --- /dev/null +++ b/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; +import { Loader2 } from "lucide-react"; +import { useTranslations } from "next-intl"; + +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Progress } from "@/components/ui/progress"; +import { pctOf, type UpdatePhase } from "@/lib/desktop/updater"; +import { useUpdateInstall } from "@/lib/desktop/use-update-install"; + +function IndeterminateBar() { + return ( +
+ +
+ ); +} + +export function UpdateProgressModal() { + const t = useTranslations("DesktopUpdate"); + const { status, error, visible, dismiss, reopen, retry, close } = useUpdateInstall(); + + if (!status) return null; + + const pct = pctOf(status); + const phase: UpdatePhase = status.phase; + const locked = phase === "restarting"; // force-open, non-dismissible + const open = locked || (visible && !error) || (!!error && visible); + + const label = + error != null + ? t("failed") + : phase === "installing" + ? t("installing") + : phase === "restarting" + ? t("restarting") + : pct !== null + ? t("downloadingPct", { pct }) + : t("downloading"); + + // Corner pill when the run is active but the modal is dismissed (no error). + if (!open && !error) { + return ( + + ); + } + + return ( + { + if (locked || error) return; // restart & errors don't backdrop-dismiss + if (!o) dismiss(); + }} + > + + + {t("title")} + + + {error != null ? ( +
+

{error}

+
+ + +
+
+ ) : ( +
+ {pct !== null && phase === "downloading" ? ( + + ) : ( + + )} + + + {label} + + +

{t("reassurance")}

+
+ )} +
+
+ ); +} From b9f8264ed3708e3d3d93dfdd9b026cdbd76fb4e5 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Thu, 23 Jul 2026 22:23:26 +0530 Subject: [PATCH 07/11] fix(updater): error state always shows modal; steadier progress animation --- .../src/components/desktop/update-progress-modal.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx b/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx index 56c5dac9..86078f79 100644 --- a/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx +++ b/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx @@ -31,7 +31,7 @@ export function UpdateProgressModal() { const pct = pctOf(status); const phase: UpdatePhase = status.phase; const locked = phase === "restarting"; // force-open, non-dismissible - const open = locked || (visible && !error) || (!!error && visible); + const open = locked || visible || error != null; const label = error != null @@ -45,7 +45,7 @@ export function UpdateProgressModal() { : t("downloading"); // Corner pill when the run is active but the modal is dismissed (no error). - if (!open && !error) { + if (!open) { return ( - !installing && setOpen(o)}> + setOpen(o)}> Updates @@ -94,24 +78,10 @@ export function DesktopUpdateDialog() { {update.notes}

) : null} - {installing ? ( -
-
-
-
-

- {percent !== null ? `Downloading… ${percent}%` : "Downloading…"} -

-
- ) : ( - - )} +
) : (
From 1aaf302e1054a020ba69f4e259e376010d7da84b Mon Sep 17 00:00:00 2001 From: AKHIL Date: Thu, 23 Jul 2026 22:36:55 +0530 Subject: [PATCH 10/11] i18n(updater): add DesktopUpdate namespace across 27 locales --- apps/desktop-ui/messages/af.json | 13 +++++++++++++ apps/desktop-ui/messages/ar.json | 13 +++++++++++++ apps/desktop-ui/messages/ca.json | 13 +++++++++++++ apps/desktop-ui/messages/cs.json | 13 +++++++++++++ apps/desktop-ui/messages/da.json | 13 +++++++++++++ apps/desktop-ui/messages/de.json | 13 +++++++++++++ apps/desktop-ui/messages/el.json | 13 +++++++++++++ apps/desktop-ui/messages/en.json | 13 +++++++++++++ apps/desktop-ui/messages/es.json | 13 +++++++++++++ apps/desktop-ui/messages/fa.json | 13 +++++++++++++ apps/desktop-ui/messages/fr.json | 13 +++++++++++++ apps/desktop-ui/messages/id.json | 13 +++++++++++++ apps/desktop-ui/messages/it.json | 13 +++++++++++++ apps/desktop-ui/messages/ja.json | 13 +++++++++++++ apps/desktop-ui/messages/ko.json | 13 +++++++++++++ apps/desktop-ui/messages/ms.json | 13 +++++++++++++ apps/desktop-ui/messages/nb.json | 13 +++++++++++++ apps/desktop-ui/messages/nl.json | 13 +++++++++++++ apps/desktop-ui/messages/pl.json | 13 +++++++++++++ apps/desktop-ui/messages/pt-BR.json | 13 +++++++++++++ apps/desktop-ui/messages/pt.json | 13 +++++++++++++ apps/desktop-ui/messages/ru.json | 13 +++++++++++++ apps/desktop-ui/messages/sv.json | 13 +++++++++++++ apps/desktop-ui/messages/tr.json | 13 +++++++++++++ apps/desktop-ui/messages/uk.json | 13 +++++++++++++ apps/desktop-ui/messages/vi.json | 13 +++++++++++++ apps/desktop-ui/messages/zh.json | 13 +++++++++++++ 27 files changed, 351 insertions(+) diff --git a/apps/desktop-ui/messages/af.json b/apps/desktop-ui/messages/af.json index 3768e067..a1c5e292 100644 --- a/apps/desktop-ui/messages/af.json +++ b/apps/desktop-ui/messages/af.json @@ -4788,5 +4788,18 @@ }, "ToolPageHeader": { "offline": "100% aflyn" + }, + "DesktopUpdate": { + "title": "MyDevTools word bygewerk", + "downloading": "Opdatering word afgelaai…", + "downloadingPct": "Opdatering word afgelaai… {pct}%", + "installing": "Opdatering word geïnstalleer…", + "restarting": "MyDevTools word herbegin…", + "reassurance": "Jou vanlyn-data word nooit aangeraak nie.", + "failed": "Opdatering kon nie geïnstalleer word nie", + "retry": "Probeer weer", + "close": "Sluit", + "pill": "Word bygewerk…", + "pillPct": "Word bygewerk… {pct}%" } } diff --git a/apps/desktop-ui/messages/ar.json b/apps/desktop-ui/messages/ar.json index 71bda17a..6a70afa1 100644 --- a/apps/desktop-ui/messages/ar.json +++ b/apps/desktop-ui/messages/ar.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% دون اتصال" + }, + "DesktopUpdate": { + "title": "جارٍ تحديث MyDevTools", + "downloading": "جارٍ تنزيل التحديث…", + "downloadingPct": "جارٍ تنزيل التحديث… {pct}%", + "installing": "جارٍ تثبيت التحديث…", + "restarting": "جارٍ إعادة تشغيل MyDevTools…", + "reassurance": "بياناتك دون اتصال لا يتم المساس بها أبدًا.", + "failed": "فشل تثبيت التحديث", + "retry": "إعادة المحاولة", + "close": "إغلاق", + "pill": "جارٍ التحديث…", + "pillPct": "جارٍ التحديث… {pct}%" } } diff --git a/apps/desktop-ui/messages/ca.json b/apps/desktop-ui/messages/ca.json index 80267ab9..400d3e11 100644 --- a/apps/desktop-ui/messages/ca.json +++ b/apps/desktop-ui/messages/ca.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% fora de línia" + }, + "DesktopUpdate": { + "title": "S'està actualitzant MyDevTools", + "downloading": "Baixant l'actualització…", + "downloadingPct": "Baixant l'actualització… {pct}%", + "installing": "Instal·lant l'actualització…", + "restarting": "Reiniciant MyDevTools…", + "reassurance": "Les teves dades sense connexió mai es toquen.", + "failed": "No s'ha pogut instal·lar l'actualització", + "retry": "Torna-ho a provar", + "close": "Tanca", + "pill": "S'està actualitzant…", + "pillPct": "S'està actualitzant… {pct}%" } } diff --git a/apps/desktop-ui/messages/cs.json b/apps/desktop-ui/messages/cs.json index 023c2625..fc80c3c1 100644 --- a/apps/desktop-ui/messages/cs.json +++ b/apps/desktop-ui/messages/cs.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% offline" + }, + "DesktopUpdate": { + "title": "Aktualizace MyDevTools", + "downloading": "Stahování aktualizace…", + "downloadingPct": "Stahování aktualizace… {pct}%", + "installing": "Instalace aktualizace…", + "restarting": "Restartování MyDevTools…", + "reassurance": "Vašich offline dat se nikdy nedotkneme.", + "failed": "Aktualizaci se nepodařilo nainstalovat", + "retry": "Zkusit znovu", + "close": "Zavřít", + "pill": "Aktualizace…", + "pillPct": "Aktualizace… {pct}%" } } diff --git a/apps/desktop-ui/messages/da.json b/apps/desktop-ui/messages/da.json index 33eea23b..ae9a71f6 100644 --- a/apps/desktop-ui/messages/da.json +++ b/apps/desktop-ui/messages/da.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100 % offline" + }, + "DesktopUpdate": { + "title": "Opdaterer MyDevTools", + "downloading": "Downloader opdatering…", + "downloadingPct": "Downloader opdatering… {pct} %", + "installing": "Installerer opdatering…", + "restarting": "Genstarter MyDevTools…", + "reassurance": "Dine offline-data bliver aldrig rørt.", + "failed": "Opdateringen kunne ikke installeres", + "retry": "Prøv igen", + "close": "Luk", + "pill": "Opdaterer…", + "pillPct": "Opdaterer… {pct} %" } } diff --git a/apps/desktop-ui/messages/de.json b/apps/desktop-ui/messages/de.json index 75cbaa04..433b93f6 100644 --- a/apps/desktop-ui/messages/de.json +++ b/apps/desktop-ui/messages/de.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100 % offline" + }, + "DesktopUpdate": { + "title": "MyDevTools wird aktualisiert", + "downloading": "Update wird heruntergeladen…", + "downloadingPct": "Update wird heruntergeladen… {pct} %", + "installing": "Update wird installiert…", + "restarting": "MyDevTools wird neu gestartet…", + "reassurance": "Deine Offline-Daten werden niemals verändert.", + "failed": "Update konnte nicht installiert werden", + "retry": "Erneut versuchen", + "close": "Schließen", + "pill": "Wird aktualisiert…", + "pillPct": "Wird aktualisiert… {pct} %" } } diff --git a/apps/desktop-ui/messages/el.json b/apps/desktop-ui/messages/el.json index 37da4c00..2bf66c30 100644 --- a/apps/desktop-ui/messages/el.json +++ b/apps/desktop-ui/messages/el.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% εκτός σύνδεσης" + }, + "DesktopUpdate": { + "title": "Ενημέρωση MyDevTools", + "downloading": "Λήψη ενημέρωσης…", + "downloadingPct": "Λήψη ενημέρωσης… {pct}%", + "installing": "Εγκατάσταση ενημέρωσης…", + "restarting": "Επανεκκίνηση του MyDevTools…", + "reassurance": "Τα δεδομένα σας εκτός σύνδεσης δεν επηρεάζονται ποτέ.", + "failed": "Η εγκατάσταση της ενημέρωσης απέτυχε", + "retry": "Επανάληψη", + "close": "Κλείσιμο", + "pill": "Ενημέρωση…", + "pillPct": "Ενημέρωση… {pct}%" } } diff --git a/apps/desktop-ui/messages/en.json b/apps/desktop-ui/messages/en.json index 9129035e..141a11f6 100644 --- a/apps/desktop-ui/messages/en.json +++ b/apps/desktop-ui/messages/en.json @@ -4813,5 +4813,18 @@ }, "ToolPageHeader": { "offline": "100% offline" + }, + "DesktopUpdate": { + "title": "Updating MyDevTools", + "downloading": "Downloading update…", + "downloadingPct": "Downloading update… {pct}%", + "installing": "Installing update…", + "restarting": "Restarting MyDevTools…", + "reassurance": "Your offline data is never touched.", + "failed": "Update failed to install", + "retry": "Retry", + "close": "Close", + "pill": "Updating…", + "pillPct": "Updating… {pct}%" } } diff --git a/apps/desktop-ui/messages/es.json b/apps/desktop-ui/messages/es.json index a5fbed68..a4436952 100644 --- a/apps/desktop-ui/messages/es.json +++ b/apps/desktop-ui/messages/es.json @@ -4788,5 +4788,18 @@ }, "ToolPageHeader": { "offline": "100% sin conexión" + }, + "DesktopUpdate": { + "title": "Actualizando MyDevTools", + "downloading": "Descargando actualización…", + "downloadingPct": "Descargando actualización… {pct}%", + "installing": "Instalando actualización…", + "restarting": "Reiniciando MyDevTools…", + "reassurance": "Tus datos sin conexión nunca se modifican.", + "failed": "No se pudo instalar la actualización", + "retry": "Reintentar", + "close": "Cerrar", + "pill": "Actualizando…", + "pillPct": "Actualizando… {pct}%" } } diff --git a/apps/desktop-ui/messages/fa.json b/apps/desktop-ui/messages/fa.json index acd2fe02..d8d0dcff 100644 --- a/apps/desktop-ui/messages/fa.json +++ b/apps/desktop-ui/messages/fa.json @@ -4788,5 +4788,18 @@ }, "ToolPageHeader": { "offline": "۱۰۰٪ آفلاین" + }, + "DesktopUpdate": { + "title": "در حال به‌روزرسانی MyDevTools", + "downloading": "در حال دانلود به‌روزرسانی…", + "downloadingPct": "در حال دانلود به‌روزرسانی… {pct}٪", + "installing": "در حال نصب به‌روزرسانی…", + "restarting": "در حال راه‌اندازی مجدد MyDevTools…", + "reassurance": "داده‌های آفلاین شما هرگز دستکاری نمی‌شوند.", + "failed": "نصب به‌روزرسانی ناموفق بود", + "retry": "تلاش دوباره", + "close": "بستن", + "pill": "در حال به‌روزرسانی…", + "pillPct": "در حال به‌روزرسانی… {pct}٪" } } diff --git a/apps/desktop-ui/messages/fr.json b/apps/desktop-ui/messages/fr.json index faf08b59..49af343c 100644 --- a/apps/desktop-ui/messages/fr.json +++ b/apps/desktop-ui/messages/fr.json @@ -4788,5 +4788,18 @@ }, "ToolPageHeader": { "offline": "100 % hors ligne" + }, + "DesktopUpdate": { + "title": "Mise à jour de MyDevTools", + "downloading": "Téléchargement de la mise à jour…", + "downloadingPct": "Téléchargement de la mise à jour… {pct} %", + "installing": "Installation de la mise à jour…", + "restarting": "Redémarrage de MyDevTools…", + "reassurance": "Vos données hors ligne ne sont jamais touchées.", + "failed": "Échec de l'installation de la mise à jour", + "retry": "Réessayer", + "close": "Fermer", + "pill": "Mise à jour…", + "pillPct": "Mise à jour… {pct} %" } } diff --git a/apps/desktop-ui/messages/id.json b/apps/desktop-ui/messages/id.json index a8581aa4..361df748 100644 --- a/apps/desktop-ui/messages/id.json +++ b/apps/desktop-ui/messages/id.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% offline" + }, + "DesktopUpdate": { + "title": "Memperbarui MyDevTools", + "downloading": "Mengunduh pembaruan…", + "downloadingPct": "Mengunduh pembaruan… {pct}%", + "installing": "Menginstal pembaruan…", + "restarting": "Memulai ulang MyDevTools…", + "reassurance": "Data offline Anda tidak akan pernah disentuh.", + "failed": "Pembaruan gagal diinstal", + "retry": "Coba lagi", + "close": "Tutup", + "pill": "Memperbarui…", + "pillPct": "Memperbarui… {pct}%" } } diff --git a/apps/desktop-ui/messages/it.json b/apps/desktop-ui/messages/it.json index d103eb86..0ee93ac9 100644 --- a/apps/desktop-ui/messages/it.json +++ b/apps/desktop-ui/messages/it.json @@ -4788,5 +4788,18 @@ }, "ToolPageHeader": { "offline": "100% offline" + }, + "DesktopUpdate": { + "title": "Aggiornamento di MyDevTools", + "downloading": "Download dell'aggiornamento…", + "downloadingPct": "Download dell'aggiornamento… {pct}%", + "installing": "Installazione dell'aggiornamento…", + "restarting": "Riavvio di MyDevTools…", + "reassurance": "I tuoi dati offline non vengono mai toccati.", + "failed": "Installazione dell'aggiornamento non riuscita", + "retry": "Riprova", + "close": "Chiudi", + "pill": "Aggiornamento…", + "pillPct": "Aggiornamento… {pct}%" } } diff --git a/apps/desktop-ui/messages/ja.json b/apps/desktop-ui/messages/ja.json index 85f79c0f..ab30105b 100644 --- a/apps/desktop-ui/messages/ja.json +++ b/apps/desktop-ui/messages/ja.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "完全オフライン" + }, + "DesktopUpdate": { + "title": "MyDevTools を更新しています", + "downloading": "更新をダウンロードしています…", + "downloadingPct": "更新をダウンロードしています… {pct}%", + "installing": "更新をインストールしています…", + "restarting": "MyDevTools を再起動しています…", + "reassurance": "オフラインデータが変更されることはありません。", + "failed": "更新のインストールに失敗しました", + "retry": "再試行", + "close": "閉じる", + "pill": "更新中…", + "pillPct": "更新中… {pct}%" } } diff --git a/apps/desktop-ui/messages/ko.json b/apps/desktop-ui/messages/ko.json index 5bf43be3..7aaffd99 100644 --- a/apps/desktop-ui/messages/ko.json +++ b/apps/desktop-ui/messages/ko.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% 오프라인" + }, + "DesktopUpdate": { + "title": "MyDevTools 업데이트 중", + "downloading": "업데이트 다운로드 중…", + "downloadingPct": "업데이트 다운로드 중… {pct}%", + "installing": "업데이트 설치 중…", + "restarting": "MyDevTools 재시작 중…", + "reassurance": "오프라인 데이터는 절대 변경되지 않습니다.", + "failed": "업데이트 설치에 실패했습니다", + "retry": "다시 시도", + "close": "닫기", + "pill": "업데이트 중…", + "pillPct": "업데이트 중… {pct}%" } } diff --git a/apps/desktop-ui/messages/ms.json b/apps/desktop-ui/messages/ms.json index efdb7c80..a416ab36 100644 --- a/apps/desktop-ui/messages/ms.json +++ b/apps/desktop-ui/messages/ms.json @@ -4788,5 +4788,18 @@ }, "ToolPageHeader": { "offline": "100% luar talian" + }, + "DesktopUpdate": { + "title": "Mengemas kini MyDevTools", + "downloading": "Memuat turun kemas kini…", + "downloadingPct": "Memuat turun kemas kini… {pct}%", + "installing": "Memasang kemas kini…", + "restarting": "Memulakan semula MyDevTools…", + "reassurance": "Data luar talian anda tidak pernah disentuh.", + "failed": "Kemas kini gagal dipasang", + "retry": "Cuba semula", + "close": "Tutup", + "pill": "Mengemas kini…", + "pillPct": "Mengemas kini… {pct}%" } } diff --git a/apps/desktop-ui/messages/nb.json b/apps/desktop-ui/messages/nb.json index 43291f64..245d980d 100644 --- a/apps/desktop-ui/messages/nb.json +++ b/apps/desktop-ui/messages/nb.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100 % offline" + }, + "DesktopUpdate": { + "title": "Oppdaterer MyDevTools", + "downloading": "Laster ned oppdatering…", + "downloadingPct": "Laster ned oppdatering… {pct} %", + "installing": "Installerer oppdatering…", + "restarting": "Starter MyDevTools på nytt…", + "reassurance": "De offline dataene dine blir aldri rørt.", + "failed": "Oppdateringen kunne ikke installeres", + "retry": "Prøv igjen", + "close": "Lukk", + "pill": "Oppdaterer…", + "pillPct": "Oppdaterer… {pct} %" } } diff --git a/apps/desktop-ui/messages/nl.json b/apps/desktop-ui/messages/nl.json index f3e4dc0f..5bb1d08b 100644 --- a/apps/desktop-ui/messages/nl.json +++ b/apps/desktop-ui/messages/nl.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% offline" + }, + "DesktopUpdate": { + "title": "MyDevTools wordt bijgewerkt", + "downloading": "Update wordt gedownload…", + "downloadingPct": "Update wordt gedownload… {pct}%", + "installing": "Update wordt geïnstalleerd…", + "restarting": "MyDevTools wordt opnieuw opgestart…", + "reassurance": "Je offline gegevens worden nooit aangeraakt.", + "failed": "Update kon niet worden geïnstalleerd", + "retry": "Opnieuw proberen", + "close": "Sluiten", + "pill": "Bijwerken…", + "pillPct": "Bijwerken… {pct}%" } } diff --git a/apps/desktop-ui/messages/pl.json b/apps/desktop-ui/messages/pl.json index 55901787..3c49c6b4 100644 --- a/apps/desktop-ui/messages/pl.json +++ b/apps/desktop-ui/messages/pl.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% offline" + }, + "DesktopUpdate": { + "title": "Aktualizowanie MyDevTools", + "downloading": "Pobieranie aktualizacji…", + "downloadingPct": "Pobieranie aktualizacji… {pct}%", + "installing": "Instalowanie aktualizacji…", + "restarting": "Ponowne uruchamianie MyDevTools…", + "reassurance": "Twoje dane offline nigdy nie są naruszane.", + "failed": "Nie udało się zainstalować aktualizacji", + "retry": "Spróbuj ponownie", + "close": "Zamknij", + "pill": "Aktualizowanie…", + "pillPct": "Aktualizowanie… {pct}%" } } diff --git a/apps/desktop-ui/messages/pt-BR.json b/apps/desktop-ui/messages/pt-BR.json index 46c8b9d8..c208a94d 100644 --- a/apps/desktop-ui/messages/pt-BR.json +++ b/apps/desktop-ui/messages/pt-BR.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% offline" + }, + "DesktopUpdate": { + "title": "Atualizando o MyDevTools", + "downloading": "Baixando atualização…", + "downloadingPct": "Baixando atualização… {pct}%", + "installing": "Instalando atualização…", + "restarting": "Reiniciando o MyDevTools…", + "reassurance": "Seus dados offline nunca são alterados.", + "failed": "Falha ao instalar a atualização", + "retry": "Tentar novamente", + "close": "Fechar", + "pill": "Atualizando…", + "pillPct": "Atualizando… {pct}%" } } diff --git a/apps/desktop-ui/messages/pt.json b/apps/desktop-ui/messages/pt.json index d208f63f..243ed3dc 100644 --- a/apps/desktop-ui/messages/pt.json +++ b/apps/desktop-ui/messages/pt.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% offline" + }, + "DesktopUpdate": { + "title": "A atualizar o MyDevTools", + "downloading": "A transferir a atualização…", + "downloadingPct": "A transferir a atualização… {pct}%", + "installing": "A instalar a atualização…", + "restarting": "A reiniciar o MyDevTools…", + "reassurance": "Os seus dados offline nunca são alterados.", + "failed": "Falha ao instalar a atualização", + "retry": "Tentar novamente", + "close": "Fechar", + "pill": "A atualizar…", + "pillPct": "A atualizar… {pct}%" } } diff --git a/apps/desktop-ui/messages/ru.json b/apps/desktop-ui/messages/ru.json index a1e95768..81272a0e 100644 --- a/apps/desktop-ui/messages/ru.json +++ b/apps/desktop-ui/messages/ru.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% офлайн" + }, + "DesktopUpdate": { + "title": "Обновление MyDevTools", + "downloading": "Загрузка обновления…", + "downloadingPct": "Загрузка обновления… {pct}%", + "installing": "Установка обновления…", + "restarting": "Перезапуск MyDevTools…", + "reassurance": "Ваши офлайн-данные никогда не затрагиваются.", + "failed": "Не удалось установить обновление", + "retry": "Повторить", + "close": "Закрыть", + "pill": "Обновление…", + "pillPct": "Обновление… {pct}%" } } diff --git a/apps/desktop-ui/messages/sv.json b/apps/desktop-ui/messages/sv.json index 65237956..4dda261b 100644 --- a/apps/desktop-ui/messages/sv.json +++ b/apps/desktop-ui/messages/sv.json @@ -4788,5 +4788,18 @@ }, "ToolPageHeader": { "offline": "100 % offline" + }, + "DesktopUpdate": { + "title": "Uppdaterar MyDevTools", + "downloading": "Laddar ner uppdatering…", + "downloadingPct": "Laddar ner uppdatering… {pct} %", + "installing": "Installerar uppdatering…", + "restarting": "Startar om MyDevTools…", + "reassurance": "Din offlinedata rörs aldrig.", + "failed": "Det gick inte att installera uppdateringen", + "retry": "Försök igen", + "close": "Stäng", + "pill": "Uppdaterar…", + "pillPct": "Uppdaterar… {pct} %" } } diff --git a/apps/desktop-ui/messages/tr.json b/apps/desktop-ui/messages/tr.json index 7b87022b..d1d6a5fb 100644 --- a/apps/desktop-ui/messages/tr.json +++ b/apps/desktop-ui/messages/tr.json @@ -4788,5 +4788,18 @@ }, "ToolPageHeader": { "offline": "%100 çevrimdışı" + }, + "DesktopUpdate": { + "title": "MyDevTools güncelleniyor", + "downloading": "Güncelleme indiriliyor…", + "downloadingPct": "Güncelleme indiriliyor… %{pct}", + "installing": "Güncelleme yükleniyor…", + "restarting": "MyDevTools yeniden başlatılıyor…", + "reassurance": "Çevrimdışı verileriniz asla değiştirilmez.", + "failed": "Güncelleme yüklenemedi", + "retry": "Yeniden dene", + "close": "Kapat", + "pill": "Güncelleniyor…", + "pillPct": "Güncelleniyor… %{pct}" } } diff --git a/apps/desktop-ui/messages/uk.json b/apps/desktop-ui/messages/uk.json index 5a2791f5..3582164d 100644 --- a/apps/desktop-ui/messages/uk.json +++ b/apps/desktop-ui/messages/uk.json @@ -4788,5 +4788,18 @@ }, "ToolPageHeader": { "offline": "100% офлайн" + }, + "DesktopUpdate": { + "title": "Оновлення MyDevTools", + "downloading": "Завантаження оновлення…", + "downloadingPct": "Завантаження оновлення… {pct}%", + "installing": "Встановлення оновлення…", + "restarting": "Перезапуск MyDevTools…", + "reassurance": "Ваші офлайн-дані ніколи не змінюються.", + "failed": "Не вдалося встановити оновлення", + "retry": "Повторити", + "close": "Закрити", + "pill": "Оновлення…", + "pillPct": "Оновлення… {pct}%" } } diff --git a/apps/desktop-ui/messages/vi.json b/apps/desktop-ui/messages/vi.json index 3793b53e..60580e20 100644 --- a/apps/desktop-ui/messages/vi.json +++ b/apps/desktop-ui/messages/vi.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% ngoại tuyến" + }, + "DesktopUpdate": { + "title": "Đang cập nhật MyDevTools", + "downloading": "Đang tải bản cập nhật…", + "downloadingPct": "Đang tải bản cập nhật… {pct}%", + "installing": "Đang cài đặt bản cập nhật…", + "restarting": "Đang khởi động lại MyDevTools…", + "reassurance": "Dữ liệu ngoại tuyến của bạn không bao giờ bị ảnh hưởng.", + "failed": "Cài đặt bản cập nhật không thành công", + "retry": "Thử lại", + "close": "Đóng", + "pill": "Đang cập nhật…", + "pillPct": "Đang cập nhật… {pct}%" } } diff --git a/apps/desktop-ui/messages/zh.json b/apps/desktop-ui/messages/zh.json index dd70045f..5088ac61 100644 --- a/apps/desktop-ui/messages/zh.json +++ b/apps/desktop-ui/messages/zh.json @@ -4873,5 +4873,18 @@ }, "ToolPageHeader": { "offline": "100% 离线" + }, + "DesktopUpdate": { + "title": "正在更新 MyDevTools", + "downloading": "正在下载更新…", + "downloadingPct": "正在下载更新… {pct}%", + "installing": "正在安装更新…", + "restarting": "正在重新启动 MyDevTools…", + "reassurance": "您的离线数据永远不会被修改。", + "failed": "更新安装失败", + "retry": "重试", + "close": "关闭", + "pill": "正在更新…", + "pillPct": "正在更新… {pct}%" } } From 645dc7d0aa5af3cf2fbc5380a287c41fc31d2485 Mon Sep 17 00:00:00 2001 From: AKHIL Date: Thu, 23 Jul 2026 22:45:37 +0530 Subject: [PATCH 11/11] fix(updater): surface translated failure title; drop bogus install-phase pill % Co-Authored-By: Claude Opus 4.8 (1M context) --- .../desktop/update-progress-modal.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx b/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx index 86078f79..b86d0e3c 100644 --- a/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx +++ b/apps/desktop-ui/src/components/desktop/update-progress-modal.tsx @@ -34,15 +34,13 @@ export function UpdateProgressModal() { const open = locked || visible || error != null; const label = - error != null - ? t("failed") - : phase === "installing" - ? t("installing") - : phase === "restarting" - ? t("restarting") - : pct !== null - ? t("downloadingPct", { pct }) - : t("downloading"); + phase === "installing" + ? t("installing") + : phase === "restarting" + ? t("restarting") + : pct !== null + ? t("downloadingPct", { pct }) + : t("downloading"); // Corner pill when the run is active but the modal is dismissed (no error). if (!open) { @@ -53,7 +51,7 @@ export function UpdateProgressModal() { className="fixed bottom-4 right-4 z-50 flex items-center gap-2 rounded-full border border-border bg-card px-3.5 py-2 text-xs font-medium shadow-lg" > - {pct !== null ? t("pillPct", { pct }) : t("pill")} + {pct !== null && phase === "downloading" ? t("pillPct", { pct }) : t("pill")} ); } @@ -68,7 +66,7 @@ export function UpdateProgressModal() { > - {t("title")} + {error != null ? t("failed") : t("title")} {error != null ? ( @@ -88,7 +86,7 @@ export function UpdateProgressModal() { )}