diff --git a/CLAUDE.md b/CLAUDE.md index b76355d4..ba9a9f18 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Claude Code Working Guide -_Last synced: v2.8.21 (2026-10-23) — Claude Docs docs and session documents (.pptx/.docx/.xlsx/.pdf/.html) land in Artifacts, unowned rows are claimed; a plugin update reports its running step (`plugin_update_progress`); a hidden update or clone leaves a progress chip._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session. +_Last synced: v2.8.22 (2026-10-23) — Users' today/7/30-day spend is booked like the cost routes (every tool counts); a page adopts a running plugin update and keeps its progress chip._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session. ## Project mission - Maintain a reliable local-first dashboard for Claude Code session monitoring. diff --git a/client/src/components/UpdateNotifier.tsx b/client/src/components/UpdateNotifier.tsx index 1e18908d..ed4908cf 100644 --- a/client/src/components/UpdateNotifier.tsx +++ b/client/src/components/UpdateNotifier.tsx @@ -19,9 +19,12 @@ * background process can do for it. Escape, the backdrop and × close it for * this page load only — also while an update applies: it keeps running in the * background (the page still reloads itself once the new server is up), and a - * failure re-opens the modal with its error. While hidden, a small chip in the - * corner keeps showing the update — with the plugin chain's current step from - * `plugin_update_progress` — and a click on it shows the modal again; "Skip this version" is the one + * failure re-opens the modal with its error. While hidden — or once the status + * stops offering the update mid-run — a small chip in the corner keeps showing + * it, with the plugin chain's current step from `plugin_update_progress`, and a + * click on it shows the modal again. A chain the server reports as running + * (started before a reload or from another tab) is adopted the same way; + * "Skip this version" is the one * persistent, per-version dismiss (never while applying). After a failed update, "Auto fix" hands the * error to an AI agent: it opens (or adopts) a workspace on the install folder * and starts a Claude Code session there with the error as its prompt — the @@ -242,13 +245,14 @@ export function UpdateNotifier() { status?.latest_release || status?.remote_sha || (installed ? `installed:${installed.root}` : null); - // Updating replaces the host's install: only an admin is offered it. + // Updating replaces the host's install: only an admin is offered it. A + // running update stays shown (unless closed) even once the status stops + // offering it — the new version may already be on disk before it restarts. const show = Boolean( isAdmin && - status?.update_available && dismissKey && - dismissedSha !== dismissKey && - closedKey !== dismissKey + closedKey !== dismissKey && + (applying || restarting || (status?.update_available && dismissedSha !== dismissKey)) ); const busy = applying || restarting || fixing; @@ -344,6 +348,19 @@ export function UpdateNotifier() { [syncFromPayload, t] ); + // A chain already running on the server (started before a reload, in another + // tab, or from the old bundle) is adopted: this tab shows its steps, then + // reloads onto the new server like the tab that started it. + const running = status?.plugin_update_progress ?? null; + const adoptedRef = useRef(false); + useEffect(() => { + if (!running || !isAdmin || applying || restarting || adoptedRef.current) return; + adoptedRef.current = true; + setProgress(running); + setRestarting(true); + pollUntilBack(true, status?.local_version ?? null); + }, [running, isAdmin, applying, restarting, pollUntilBack, status?.local_version]); + const applyNow = async () => { if (applying || restarting) return; setError(null); @@ -406,8 +423,9 @@ export function UpdateNotifier() { }) : null; - // Hidden while it runs: a chip keeps it in sight and brings the modal back. - if (!show && isAdmin && (applying || restarting) && dismissKey && closedKey === dismissKey) { + // Hidden while it runs — closed, or the status no longer offers the update + // (the new version is already on disk): a chip keeps it in sight. + if (!show && isAdmin && (applying || restarting)) { return (