feat(web): add update-available banner to sidebar footer#31316
feat(web): add update-available banner to sidebar footer#31316Jasonnnz wants to merge 6 commits into
Conversation
Show a banner in the sidebar footer when a newer assistant release is available. The banner displays the assistant avatar, the new version number, and Upgrade now / Upgrade later buttons. Dismissal is persisted per-version in localStorage so the banner reappears only for new releases. Wired into ChatLayout via the existing footerBanner prop on AssistantSideMenu. Co-Authored-By: Jason Zhou <jasonczhou3@gmail.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 290d9bfb49
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| lifecycle.assistantId ? ( | ||
| <UpdateAvailableSidebarEntry assistantId={lifecycle.assistantId} /> | ||
| ) : null, |
There was a problem hiding this comment.
Gate footer banner prop on actual visibility
updateBanner is always a truthy React element whenever assistantId exists, even when UpdateAvailableSidebarEntry renders null. AssistantSideMenu uses footerBanner truthiness to decide whether to render SideMenu.Footer and its separator, so users with no update available still get an empty footer/separator artifact. This regresses sidebar layout for the common case and should be gated by actual banner visibility before passing the prop.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 45d39b5. Extracted useIsUpdateBannerVisible() hook that ChatLayout uses to gate the footerBanner prop. When no update is available (or it's been dismissed via localStorage), updateBanner is undefined (falsy), so the sidebar's footerBanner || footerAction check correctly skips rendering SideMenu.Footer.
Additionally, the component now calls onVisibilityChange(false) when dismissed, so ChatLayout sets updateBannerDismissed = true and immediately clears the prop.
| setDismissed(true); | ||
| }, [latestVersion]); | ||
|
|
||
| if (!upgradeAvailable || dismissed || isDismissedForVersion) { |
There was a problem hiding this comment.
Reset dismiss state when latest version changes
The component sets dismissed to true on any dismiss action and then hard-blocks rendering via dismissed || isDismissedForVersion. Because dismissed is not keyed to latestVersion, a newly released version discovered later in the same session remains hidden until remount/reload, which breaks the intended per-version dismissal behavior. Visibility should be derived from the versioned record (or dismissed should be reset when latestVersion changes).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 45d39b5. Three changes:
- Dismiss reset: Added
useEffectthat resetsdismissedtofalsewhenlatestVersionchanges, so a new release during the same session re-shows the banner. - Footer visibility gate: Extracted
useIsUpdateBannerVisible()hook used byChatLayoutto only pass thefooterBannerprop when an update is actually available and not dismissed. This prevents the emptySideMenu.Footer+ separator artifact. onVisibilityChangecallback: The component notifies the parent when it hides (e.g. user clicks dismiss), soChatLayoutimmediately clears the prop rather than waiting for a re-render cycle.
| assistantId, | ||
| }: UpdateAvailableSidebarEntryProps) { | ||
| const queryClient = useQueryClient(); | ||
| const [dismissed, setDismissed] = useState(false); |
There was a problem hiding this comment.
🟡 dismissed state is never reset when latestVersion changes, hiding banner for genuinely new releases
The dismissed state at update-available-sidebar-entry.tsx:62 is a simple boolean initialized to false and set to true in handleDismiss (update-available-sidebar-entry.tsx:160), but it is never reset back to false when latestVersion changes. The guard at line 163 checks !upgradeAvailable || dismissed || isDismissedForVersion — so once dismissed is true, it permanently suppresses the banner for the lifetime of the component, even if a newer release ships.
Scenario: user dismisses the v2.0.0 banner → dismissed = true, localStorage records "2.0.0". Later, v3.0.0 is released and React Query refetches. isDismissedForVersion correctly recalculates to false (localStorage has "2.0.0" ≠ "3.0.0"), but dismissed is still true, so the component returns null. The banner for v3.0.0 won't appear until the component remounts (page reload or navigation).
| const [dismissed, setDismissed] = useState(false); | |
| const [dismissedVersion, setDismissedVersion] = useState<string | null>(null); |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 45d39b5. Added useEffect(() => setDismissed(false), [latestVersion]) so the in-memory dismiss state resets when a new version arrives. Combined with the isDismissedForVersion check (which reads the versioned localStorage record), a newer release will correctly re-show the banner.
| useQuery({ | ||
| ...assistantsRetrieveOptions({ path: { id: assistantId } }), | ||
| refetchInterval: isPollingUpgrade | ||
| ? (query) => | ||
| pollRefetchInterval(query.state.data?.current_release_version) | ||
| : false, | ||
| }); |
There was a problem hiding this comment.
🚩 No timeout on upgrade polling — indefinite refetch if backend upgrade silently fails
The polling at update-available-sidebar-entry.tsx:86-92 refetches every 3 seconds while isPollingUpgrade is true, stopping only when the assistant's current_release_version matches targetVersionRef.current. If the backend upgrade fails silently (no error thrown, version never changes), polling continues indefinitely. Consider adding a max poll count or timeout (e.g., 60 seconds) to set isPollingUpgrade = false and show an error toast. The X dismiss button is still clickable during upgrade and will hide the banner (but hooks continue running), so the user has a manual escape hatch, but the stale polling query would persist until unmount.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed in 45d39b5. Added a 60-second timeout (POLL_TIMEOUT_MS). If the version hasn't updated within that window, polling stops and a toast notifies the user: "Update is taking longer than expected. Please check Settings." The pollStartedAtRef is set when polling begins and checked each interval cycle.
… poll timeout Co-Authored-By: Jason Zhou <jasonczhou3@gmail.com>
Adds `truncate` class to the version text so long pre-release version strings (e.g. 0.8.3-local.20260520144955.4...) are clipped with an ellipsis instead of overflowing the sidebar. A `title` attribute preserves the full version on hover. Co-Authored-By: Jason Zhou <jasonczhou3@gmail.com>
…w divs Top row: avatar + title + X dismiss button Bottom row: Upgrade now + Upgrade later buttons Parent container uses flex-direction column Co-Authored-By: Jason Zhou <jasonczhou3@gmail.com>
Test Results — Update Available Sidebar BannerRan the web app locally (Vite dev server + Playwright CDP route interception) against the PR branch. All API endpoints mocked to simulate version mismatch, same-version, and upgrade flows. Test Results (6/6 passed)
Screenshots
Escalations
|
…lex-1 Co-Authored-By: Jason Zhou <jasonczhou3@gmail.com>
…ction) Co-Authored-By: Jason Zhou <jasonczhou3@gmail.com>
Adds a sidebar footer banner that appears when a newer assistant release is available. The banner shows the assistant avatar, version label ("New version — 0.x.x"), and two actions: "Upgrade now" (triggers the upgrade API with polling) and "Upgrade later" (dismisses). Dismissal is version-scoped via localStorage so the banner reappears for future releases.