Skip to content

web: UX overhaul — kill entry flicker, silent session restore, hub switcher, memory lifecycle#1

Merged
yejiahaoderek merged 7 commits into
mainfrom
web/ux-overhaul
Jul 7, 2026
Merged

web: UX overhaul — kill entry flicker, silent session restore, hub switcher, memory lifecycle#1
yejiahaoderek merged 7 commits into
mainfrom
web/ux-overhaul

Conversation

@yejiahaoderek

Copy link
Copy Markdown
Contributor

What

End-to-end UI/UX overhaul of the web app targeting the three long-standing flow defects plus the memory-lifecycle rough edges, driven by a full screen-by-screen audit (mobile + desktop, light + dark, Playwright/CDP frame captures).

Named defects — fixed at the root

  1. Bar renders then flicks into memory view on load — the /home → /brain → /h/personal/memories chain painted brain chrome (centered bar, conversations panel, Brain rail tab) for 300–600ms before yanking to memories. /home is now a neutral entry resolver: no bar, no active tab, no panel while it decides, with a persisted landing-surface hint for single-hop routing on returning entries. Verified frame-by-frame with CDP screencasts.
  2. Signed-in user tapping "Sign in" re-ran the whole loader chain — session lived only in localStorage, invisible to middleware. A secretless memax_session_presence cookie (set on token store/refresh/verify, cleared with the session) lets middleware route / and /login straight into the app. Measured: tap → app ~1.2s, login form never paints.
  3. Hub switcher placement + friction — desktop trigger existed only inside the topic panel on memories routes; switching held a ~1.5s dim overlay with a contradictory simultaneous "✓ Switched" toast, and could silently revert team selections to personal. Now: global hub anchor in the LeftRail (every surface) + compact mobile top-bar badge on all tabs; instant switch (overlay skipped when data is warm, single feedback); all switch paths target the destination hub's v2 slug; topic caches are hub-scoped.

Memory lifecycle

  • Edit discoverability: hover-revealed pencil + e shortcut on the detail body (⋯ menu path kept).
  • Bar: page scrim under the engaged bar (Spotlight pattern); ask-failure copy no longer contradicts the empty state; the no-results hint adapts to corpus emptiness.
  • /settings deep link actually opens the dialog (open-then-navigate; the old 100ms timer was cancelled by unmount cleanup).

Polish

Locale-safe greeting fallback (server "there" no longer leaks into zh), mobile inbox heading dedupe.

Review

High-effort multi-agent review over the branch confirmed 10 findings — including 3 serious session bugs introduced mid-branch (logout race resurrecting sessions, stale-cookie infinite loader loop, invite-token swallowing) — all fixed in the final commit and re-verified.

Test plan

  • 624 web unit tests, 68 files (incl. new middleware fast-path + route-helper + hub-readiness cases)
  • Go build/vet + handler tests
  • pnpm format && pnpm lint clean (--max-warnings 0)
  • Scripted Playwright journey (9 checks): silent restore → put w/ feedback → search → open → edit (e + save) → hub anchor on detail → switch (~250ms) → team content consistent
  • Stale-cookie recovery: presence cookie w/o tokens → one bounce, cookie cleared, login form reachable (no loop)
  • CDP frame captures: cold entry is loader → destination, no wrong-surface flash, hinted and unhinted

🤖 Generated with Claude Code

https://claude.ai/code/session_01DaVzvqvcAJiqtgASHYKZ5s

yejiahaoderek and others added 7 commits July 7, 2026 03:50
The /home → /brain → /h/personal/memories redirect chain painted the
brain surface (centered bar, conversations panel, Brain rail tab) for
~300-600ms before flicking to memories on every cold entry — the 'bar
renders then flicks into memory view' defect.

- /home becomes a client-side entry resolver: it decides between
  /brain (activated) and /h/personal/memories (everyone else) BEFORE
  any destination chrome paints, using a persisted landing-surface
  hint for instant single-hop routing on returning entries.
- New lib/landing-surface.ts owns the hint; useOnboardingActivation
  refreshes it whenever the server activation signal resolves;
  clearSession clears it with the token keys.
- route-helpers: /home is no longer a brain-view route — the shell
  renders neutral chrome (no bar, no active rail tab, no secondary
  panel) while resolving; getShellTabForPath(/home) → null.
- LeftRail renders collapsed on /home so entry doesn't play an
  expanded→collapsed width shift on arrival.
- recent-navigation: /home is no surface — no brain/memory morph
  into or out of the resolver.

Verified with CDP screencasts: cold entry now goes loader → neutral
shell → destination in one hop, both hinted and unhinted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaVzvqvcAJiqtgASHYKZ5s
A previously-signed-in user tapping 'Sign in' on the landing page went
through a full reload → login loader → brain flash → memories chain.
Root cause: the session lives in localStorage, so neither middleware
nor server components could see it — every signed-out surface had to
render first and correct itself client-side after auth init.

- New lib/session-presence.ts: a secretless marker cookie
  (memax_session_presence=1) planted whenever tokens are stored or a
  session is verified (login, token refresh, successful /me), cleared
  with the session. Existing sessions migrate on their first /me.
- middleware.ts: /, /login, /register redirect straight to the /home
  entry resolver when the cookie is present — signed-in users land in
  the app in one hop with zero credential re-entry and never see the
  marketing page or login form. Stale cookie degrades to one bounce
  through /login (auth init clears it; no loop).
- Tests: middleware presence fast-path cases + cookie/normalization
  independence.

Verified with Playwright: / with session → memories directly (landing
never paints); 'Sign in' tap from a pre-cookie landing → app in ~1.2s,
login form never visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaVzvqvcAJiqtgASHYKZ5s
Placement (audit C1): the only desktop switcher trigger lived inside
the topic panel header on memories routes — brain/agents/inbox/dreams
had no hub affordance at all, and mobile had one only on memories.
- LeftRail gains a hub anchor (badge + tooltip + popover switcher)
  under the brand mark, present on every surface — Slack/Linear
  pattern: workspace identity lives in the nav rail. Reuses
  HubIdentityChip via a new 'rail' variant so all switch/create logic
  stays in one component.
- MobileTopBar shows a compact hub badge in the trailing corner on
  non-memories tabs (memories keeps its title-position chip), opening
  the existing bottom sheet.

Behavior (C2): switching held a dimmed 'Switching to…' overlay for a
480ms minimum even when the destination was cache-warm, while a
'✓ Switched' toast fired simultaneously — contradictory double
feedback with artificial latency.
- Overlay hold is skipped when the warm-up settles within 120ms (the
  200ms fade is the whole transition); min-hold only applies to real
  waits.
- Removed the immediate success toast from the switch path — the
  overlay names the hub and the destination header is the
  confirmation.

Consistency (C3/C4):
- Switching from a non-hub route pushed '/memories', which middleware
  rewrote to /h/personal/... and silently reverted team-hub
  selections to personal. All switch paths (menu + create-hub flows)
  now navigate to the TARGET hub's v2 slug.
- topic detail/memories query keys now carry the hub scope (prefix
  pattern: writers patch via getQueriesData/invalidateQueries which
  are prefix-matching), so a cache entry fetched under one hub can
  never satisfy a read under another.
- Switcher menu rows show the memory count for every hub (the old
  'Show X's recent' hint line hid it on inactive rows).
- Dropped the dead '/home' warm contract (entry resolver renders no
  hub data).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaVzvqvcAJiqtgASHYKZ5s
…ettings deep link

Edit (audit D1): body editing lived only behind the ⋯ menu — content
looked inert. The detail body now shows a hover-revealed Edit pencil
(always visible on touch), and 'e' enters edit mode (Linear/GitHub
convention; guarded against inputs/contenteditable and modifiers).
The ⋯ menu path remains.

Bar search/ask states (D2/D3):
- Ask-failure copy said 'sources below are still available' directly
  above the 'No memories match' empty state. SynthesisSlot now picks
  errorNoSources copy when there are zero sources (en + zh keys).
- 'Save it above to start remembering.' is onboarding copy — shown
  even to users with a full corpus whose query missed. The no-results
  hint now picks by corpus emptiness across accessible hubs
  (empty → save-first copy; non-empty → 'try different words').

Bar scrim (D4): engaged desktop bar now sits on a dimmed page scrim
(Spotlight/Raycast pattern) — no more full-contrast content bleeding
through the glass pill; clicks land on the scrim and the existing
click-outside handler closes the bar in one step.

Settings deep link (D5): /settings opened the dialog on a 100ms timer
AFTER router.replace — the redirect unmounted the page and the effect
cleanup cleared the timer, so the dialog never appeared. Open first
(provider lives above the route tree), then navigate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaVzvqvcAJiqtgASHYKZ5s
- Hub greeting: the server interpolated a hardcoded English "there"
  into greeting_params.name for accounts without a display name,
  which leaked English into every non-English locale's greeting
  templates. The server now sends the name empty and the client
  substitutes a locale-appropriate fallback (en "there", zh "朋友")
  via the new hubHeader.greetingNameFallback key.
- Inbox page: the card-level "Inbox" SectionHeader now renders only
  on the popover panel surface. On /inbox the PageHeader (and the
  mobile top-bar tab title) already carry the title — three stacked
  "Inbox" labels on mobile (audit E6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaVzvqvcAJiqtgASHYKZ5s
… layers

Auth/session (review 0-2):
- Refresh-in-flight-at-logout race: both refresh flows (memax-client
  refreshAccessToken, auth tryRefresh) now drop the late response when
  the stored refresh token is gone — a torn-down session can no longer
  be resurrected (which, with the presence-cookie fast path, would
  have silently re-signed-in a logged-out browser).
- Stale presence cookie with no localStorage tokens caused an infinite
  /login↔/home loader loop; auth init's no-token branch now clears the
  cookie so the login form is reachable after exactly one bounce.
- /register is out of the middleware fast path (invite links carry
  ?invite=TOKEN that the redirect swallowed) and the fast path skips
  any request with a query string (/login?returnTo=… flow state).

Landing resolution (review 3, 4, 9):
- landingPathFor resolves memories against the ACTIVE hub slug — a
  /home entry no longer silently reverts a team-hub selection.
- Activation hook: query error now means UNKNOWN (isUnknown), never
  'activated'; neither the hook nor the resolver persists a landing
  hint on error, so a transient 500 can't misroute future cold entries.
- LandingSurfaceSync mounts the activation hook in the authed shell so
  the hint self-corrects on every surface (was stale-forever for users
  who activate while on memories).

Overlays + misc (review 5-8):
- bar-context brain jumps (⌘M, goBack) target /brain directly (morph
  restored, no resolver hop); dream-toast 'view recent' navigates to
  the target hub's memories path instead of /home.
- Bar scrim drops to z 50 so bar notifications (z 51) stay clickable.
- 'e' edit shortcut ignores keys originating inside dialogs/menus/
  popovers/listboxes.
- Greeting: server keeps sending 'there' for deployed-bundle
  back-compat; the client treats it (and '') as the no-name sentinel
  and substitutes the locale fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaVzvqvcAJiqtgASHYKZ5s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DaVzvqvcAJiqtgASHYKZ5s
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
memax Ready Ready Preview, Comment Jul 7, 2026 9:17pm

Request Review

@yejiahaoderek
yejiahaoderek merged commit 70f4138 into main Jul 7, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant