diff --git a/apps/desktop-tauri/src/components/QuotaBurndownChart.tsx b/apps/desktop-tauri/src/components/QuotaBurndownChart.tsx
index 5c62a795910..4daeae42d3a 100644
--- a/apps/desktop-tauri/src/components/QuotaBurndownChart.tsx
+++ b/apps/desktop-tauri/src/components/QuotaBurndownChart.tsx
@@ -1,6 +1,7 @@
import type { QuotaBurndownSnapshot, QuotaBurndownPoint } from "../types/bridge";
import type { LocaleKey } from "../i18n/keys";
import { formatRelativeUpdated } from "../lib/relativeTime";
+import { useNow } from "../hooks/useNow";
const CHART_WIDTH = 300;
const CHART_HEIGHT = 76;
@@ -42,12 +43,14 @@ function polyline(points: QuotaBurndownPoint[], startMs: number, endMs: number):
export default function QuotaBurndownChart({
burndown,
t,
- nowMs = Date.now(),
+ nowMs,
}: {
burndown: QuotaBurndownSnapshot;
t: (key: LocaleKey) => string;
nowMs?: number;
}) {
+ // Called before the early returns below; keeps the capture age advancing.
+ const liveNowMs = useNow();
if (burndown.samples.length === 0) return null;
const startMs = Date.parse(burndown.start);
@@ -63,7 +66,7 @@ export default function QuotaBurndownChart({
const idealEndX = xFor(Date.parse(burndown.ideal[1].capturedAt), startMs, resetMs);
const idealEndY = yFor(burndown.ideal[1].remainingPercent);
const remaining = Math.round(last.remainingPercent);
- const captureAge = formatRelativeUpdated(lastMs, t, nowMs);
+ const captureAge = formatRelativeUpdated(lastMs, t, nowMs ?? liveNowMs);
return (
diff --git a/apps/desktop-tauri/src/components/card/CardHeader.test.tsx b/apps/desktop-tauri/src/components/card/CardHeader.test.tsx
index 96739370a52..70b0248c6bb 100644
--- a/apps/desktop-tauri/src/components/card/CardHeader.test.tsx
+++ b/apps/desktop-tauri/src/components/card/CardHeader.test.tsx
@@ -125,6 +125,27 @@ describe("CardHeader", () => {
expect(screen.getByRole("button", { name: "Copy error" })).toBeInTheDocument();
});
+ it("advances the update subtitle while the data stays unchanged", () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(Date.parse("2026-10-11T12:00:00Z"));
+ render(
+
,
+ );
+ expect(screen.getByText("Updated just now")).toBeInTheDocument();
+
+ act(() => {
+ vi.advanceTimersByTime(60_000);
+ });
+ expect(screen.getByText("Updated 1m ago")).toBeInTheDocument();
+ });
+
it("omits the email and plan when there are none", () => {
const { container } = render(
,
diff --git a/apps/desktop-tauri/src/components/card/CardHeader.tsx b/apps/desktop-tauri/src/components/card/CardHeader.tsx
index 0b1f2083699..77e7bbeb65d 100644
--- a/apps/desktop-tauri/src/components/card/CardHeader.tsx
+++ b/apps/desktop-tauri/src/components/card/CardHeader.tsx
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import type { LocaleKey } from "../../i18n/keys";
import { useLocale } from "../../hooks/useLocale";
import { formatRelativeUpdated } from "../../lib/relativeTime";
+import { useNow } from "../../hooks/useNow";
type Translate = (key: LocaleKey) => string;
@@ -105,6 +106,7 @@ export default function CardHeader({
isRefreshing: boolean;
}) {
const { t } = useLocale();
+ const nowMs = useNow();
return (
@@ -130,7 +132,7 @@ export default function CardHeader({
>
) : (
- {headerSubtitle(updatedAt, isRefreshing, t)}
+ {headerSubtitle(updatedAt, isRefreshing, t, nowMs)}
)}
{plan && {plan}}
diff --git a/apps/desktop-tauri/src/hooks/useNow.test.ts b/apps/desktop-tauri/src/hooks/useNow.test.ts
new file mode 100644
index 00000000000..459102d9b1c
--- /dev/null
+++ b/apps/desktop-tauri/src/hooks/useNow.test.ts
@@ -0,0 +1,45 @@
+import { act, renderHook } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { NOW_TICK_MS, useNow } from "./useNow";
+
+describe("useNow", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(Date.parse("2026-10-11T12:00:00Z"));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("starts at the current time and re-reads it every tick", () => {
+ const { result } = renderHook(() => useNow());
+ expect(result.current).toBe(Date.parse("2026-10-11T12:00:00Z"));
+
+ act(() => {
+ vi.advanceTimersByTime(NOW_TICK_MS);
+ });
+ expect(result.current).toBe(Date.parse("2026-10-11T12:00:15Z"));
+ expect(result.current).toBe(Date.now());
+ });
+
+ it("does not re-render before the first tick", () => {
+ const seen: number[] = [];
+ renderHook(() => {
+ seen.push(useNow());
+ });
+ const before = seen.length;
+ act(() => {
+ vi.advanceTimersByTime(NOW_TICK_MS - 1);
+ });
+ expect(seen.length).toBe(before);
+ });
+
+ it("clears its interval on unmount", () => {
+ const clearSpy = vi.spyOn(window, "clearInterval");
+ const { unmount } = renderHook(() => useNow());
+ unmount();
+ expect(clearSpy).toHaveBeenCalled();
+ clearSpy.mockRestore();
+ });
+});
diff --git a/apps/desktop-tauri/src/hooks/useNow.ts b/apps/desktop-tauri/src/hooks/useNow.ts
new file mode 100644
index 00000000000..b0dc6a035e2
--- /dev/null
+++ b/apps/desktop-tauri/src/hooks/useNow.ts
@@ -0,0 +1,18 @@
+import { useEffect, useState } from "react";
+
+/** Default re-read period for relative labels such as "1 minute ago". */
+export const NOW_TICK_MS = 15_000;
+
+/**
+ * The current time in Unix ms, re-read every `intervalMs` so relative
+ * timestamps ("Updated 1 minute ago") advance while the data stays unchanged.
+ * Each call owns one interval, cleared on unmount.
+ */
+export function useNow(intervalMs: number = NOW_TICK_MS): number {
+ const [now, setNow] = useState(() => Date.now());
+ useEffect(() => {
+ const id = window.setInterval(() => setNow(Date.now()), intervalMs);
+ return () => window.clearInterval(id);
+ }, [intervalMs]);
+ return now;
+}
diff --git a/apps/desktop-tauri/src/hooks/useTrayPanelController.ts b/apps/desktop-tauri/src/hooks/useTrayPanelController.ts
index 1cdeef4a2c4..6b2b03ecd04 100644
--- a/apps/desktop-tauri/src/hooks/useTrayPanelController.ts
+++ b/apps/desktop-tauri/src/hooks/useTrayPanelController.ts
@@ -19,6 +19,7 @@ import { useProviderStatuses } from "./useProviderStatuses";
import { useSettings } from "./useSettings";
import { useUpdateState } from "./useUpdateState";
import { useLocale } from "./useLocale";
+import { useNow } from "./useNow";
import { useProviderSwitcherKeys } from "./useProviderSwitcherKeys";
import { useSurfaceTarget } from "./useSurfaceMode";
import { useTrayPanelLayout } from "./useTrayPanelLayout";
@@ -202,12 +203,15 @@ export function useTrayPanelController(state: BootstrapState) {
};
}, [actionProviderId]);
const serviceStatuses = useProviderStatuses();
+ // Re-read the clock so "updated N ago" status text keeps advancing.
+ const nowMs = useNow();
// macOS `MenuDescriptor.statusLine`: the selected provider, else the first
// enabled one, whether or not it has a usage snapshot yet.
const statusLine = serviceStatusLine(
serviceStatuses,
selectedProviderId ?? denseProviderSlots[0]?.id ?? null,
t,
+ nowMs,
);
const links =
providerLinks !== null && providerLinks.providerId === actionProviderId
diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx
index a696e27203f..0cdba3f5c54 100644
--- a/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx
+++ b/apps/desktop-tauri/src/surfaces/settings/providers/ProviderDetailPane.tsx
@@ -9,6 +9,7 @@ import { SettingsGroup } from "../../../components/FormControls";
import { formatRelativeUpdated } from "../../../lib/relativeTime";
import { nextEnabledProviders } from "../settingsTabs";
import { useLocale } from "../../../hooks/useLocale";
+import { useNow } from "../../../hooks/useNow";
import { providerAllowsPace } from "../../../lib/providerPace";
import { providerBarAccent } from "../../../components/providers/providerIcons";
import {
@@ -147,6 +148,8 @@ export function ProviderDetailPane({
// after the selection changed) must not replace the current provider.
const selectedProviderId = useRef(providerId);
selectedProviderId.current = providerId;
+ // Keeps "Updated N ago" advancing while the pane stays open.
+ const nowMs = useNow();
const load = useCallback(async (id: string, signal?: { stale: boolean }) => {
const isStale = () => signal?.stale === true || selectedProviderId.current !== id;
@@ -352,7 +355,7 @@ export function ProviderDetailPane({
const hasFetchError = Boolean(detail.lastError?.trim());
const updatedMs = detail.lastUpdated ? Date.parse(detail.lastUpdated) : Number.NaN;
const updatedText = Number.isFinite(updatedMs)
- ? formatRelativeUpdated(updatedMs, t)
+ ? formatRelativeUpdated(updatedMs, t, nowMs)
: busy
? t("SectionRefreshing")
: hasFetchError