fix(web): port homepage UI tweaks from platform #7421 (LUM-1726)#31321
Conversation
Drift port of vellum-assistant-platform#7421 (commit f216413).
- Suggestion pill icon size: 9px → 18px
- Hide feed filter bar when only one category is present (<=1)
- Filter bar → feed sections spacing: lg → sm (8px)
- Unread indicator dot: right → left of feed row icon
- Detail panel default: defaultLeftWidth={600} → defaultLeftPercent={50}
resolved via useLayoutEffect to prevent flash from pixel fallback
- Derive effectiveFilter from activeFilter + presentCategories so a
stale category filter automatically falls back to "All" instead of
stranding the feed in an empty state with no visible filter controls
Adds defaultLeftPercent prop to ResizablePanel (design library) with
useLayoutEffect-based resolution that respects existing localStorage
preferences.
https://claude.ai/code/session_013dBXRbLF218UhdLq7FEAvv
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ccd1c97383
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const stored = localStorage.getItem(storageKey); | ||
| if (stored != null) return; |
There was a problem hiding this comment.
Ignore malformed persisted widths before skipping percent default
The new defaultLeftPercent path treats any existing localStorage entry as a valid preference and returns early, even when that value is malformed (for example "NaN" or non-numeric data). In that case the useState initializer falls back to defaultLeftWidth, but the layout effect never applies the percentage default, so users can get a persistent, unintended width. Parse and validate the stored value here (same as initialization) before deciding to skip percentage resolution.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
✦ APPROVE
What this does: Drift port of platform PR #7421 (commit f216413, Jason, May 20) — 6 homepage polish fixes that hadn't been brought over. Single commit, no logic gap from the platform.
All 6 platform changes verified:
- Suggestion pill icon
size-[9px]→size-[18px]— 9px was rendering invisible on most screens ✅ - Filter bar hidden when
length <= 1— no point showing a filter with a single option ✅ - Filter bar → feed sections gap
app-spacing-lg→app-spacing-sm✅ - Unread dot
-right-0.5→-left-0.5— top-left of feed row icon ✅ defaultLeftWidth={600}→defaultLeftPercent={50}— responsive 50/50 split instead of fixed px ✅effectiveFilterstale-filter fallback — ifactiveFilternames a category that no longer exists inpresentCategories, falls back tonulland shows all items rather than stranding the feed empty ✅
useLayoutEffect in ResizablePanel — correct choice:
The new effect runs before paint to resolve a percentage-based default width, avoiding a single-frame flash of the pixel fallback. The anti-patterns KB explicitly mandates useLayoutEffect over useEffect for "set state before paint" patterns (evidence: PR #31144). The existing useEffect at line 118 (resize listener) doesn't need before-paint timing, so the two effects are correctly using different variants. Dep array [defaultLeftPercent, storageKey, clamp] is stable for static props — runs once on mount. ✅
clamp stability: Defined via useCallback([minLeftWidth, minRightWidth]) — stable for the fixed props passed at the call site. ✅
effectiveFilter derivation: Primitive result (string | null), computed fresh each render from activeFilter state + presentCategories. useMemo isn't needed since the value is a primitive and the compute is trivial — matches the platform implementation. ✅
Empty state message uses effectiveFilter: The "No items match the selected filter" branch correctly uses effectiveFilter, so a stale filter being cleared to null won't display the wrong empty state copy. ✅
No anti-patterns detected. No bot reviews yet — triggers Devin + Codex second approval.
Reviewed at ccd1c973.
| const effectiveFilter = | ||
| activeFilter && presentCategories.includes(activeFilter) | ||
| ? activeFilter | ||
| : null; |
There was a problem hiding this comment.
🚩 Stale activeFilter state can silently re-activate when categories reappear
The effectiveFilter derived state at apps/web/src/domains/home/home-feed-list.tsx:39-42 gracefully falls back to null when the selected category disappears from presentCategories. However, the underlying activeFilter React state is never reset — it retains the old category. If that category later reappears (e.g., new items arrive), effectiveFilter will snap back to the previously selected filter without user interaction. This is an edge case (requires dismissing all items of a category, then new items arriving in the same category), and could be seen as a feature ("sticky filter memory"), but it may surprise users. A useEffect that calls setActiveFilter(null) when activeFilter is not in presentCategories would make the reset explicit.
Was this helpful? React with 👍 or 👎 to provide feedback.
Two findings flagged in review, both fixed properly rather than faithful-port mirroring of platform — this repo doesn't carry forward upstream tech debt. 1. ResizablePanel (Codex P2): malformed localStorage values made the useState initializer fall back to defaultLeftWidth while the new useLayoutEffect skipped percent resolution, leaving a persistent unintended width. Extracted a readStoredWidth() helper that does shape + finiteness validation in one place and is used by both the initializer and the percent-resolution effect. 2. HomeFeedList (Devin): the previous diff resolved a stale activeFilter via the derived effectiveFilter for rendering, but never reset the underlying state. If the selected category disappeared and later reappeared, the filter would silently re-activate. Use React's adjust-state-during-render pattern to keep activeFilter in sync with effectiveFilter — React bails out on same-value setState so this avoids a synchronization Effect. https://react.dev/reference/react/useState#storing-information-from-previous-renders https://claude.ai/code/session_013dBXRbLF218UhdLq7FEAvv
Add explicit guidance to apps/web/AGENTS.md so this rule persists across agent sessions and to anyone (human or AI) working on the migration: - A faithful port preserves behavior and feature parity, not every implementation choice. The stack changed; the code should change with it. - This is an OSS repo — align with React, React Router, and major OSS players' recommended patterns rather than mirroring the platform repo. - When a bot review flags a real issue in newly-ported code, fix it rather than dismissing as "matches upstream." The platform will be deprecated; we don't need to mirror its bugs. - Small refactors belong in the port PR; large ones get a separate Linear issue. - PR descriptions should call out divergences from the platform implementation so reviewers understand the deltas. https://claude.ai/code/session_013dBXRbLF218UhdLq7FEAvv
OSS contributors working in this repo have no context on the platform repo and shouldn't need to. The "don't carry over upstream tech debt" policy belongs in the platform repo's web docs (alongside the existing migration content), not here. Reverting the section added in 845ebf0. Equivalent guidance moves to vellum-assistant-platform/web/CLAUDE.md. https://claude.ai/code/session_013dBXRbLF218UhdLq7FEAvv
Summary
Drift port of vellum-assistant-platform#7421 (commit
f216413, Jason, May 20). Brings the homepage UI in this repo back in sync with the platform repo.Tracking: LUM-1726
Investigation note
The original report was "home shows an empty placeholder instead of the rich UI that's live on dev." After diffing the two repos, the home components (
home-page,home-greeting-header,home-suggestion-pill-bar,home-feed-list,home-recap-row,home-feed-filter-bar,home-detail-panel, hooks, types, API) are all fully ported and wired up. The "Here's what's been going on" + "No items to show" the user saw locally is the normal empty state — the sidebar in that same screenshot showed zero conversations across all categories, indicating an empty local backend rather than a porting gap.What I did find was that platform PR #7421 merged the same day with 6 homepage polish fixes that had not been ported. This PR ports those.
Changes (mirror of platform commit
f216413)size-[9px]→size-[18px]length === 0→length <= 1)app-spacing-lg→app-spacing-sm-right-0.5→-left-0.5(top-left of feed row icon)defaultLeftWidth={600}→defaultLeftPercent={50}effectiveFilterfromactiveFilter+presentCategoriesso a stale category filter falls back to "All" instead of stranding the feed in an empty state with no visible filter controlsAdds new
defaultLeftPercentprop toResizablePanel(design library) withuseLayoutEffect-based resolution that respects existinglocalStoragepreferences. Resolves before paint to avoid a pixel-fallback flash.Test plan
bun run lint— clean (1 unrelated pre-existing warning inlib/errors/report.ts)bunx tsc --noEmit— clean/assistant/homehttps://claude.ai/code/session_013dBXRbLF218UhdLq7FEAvv
Generated by Claude Code