fix(web): lift assistant lifecycle to ChatLayout, fix blank pages#31212
Conversation
ChatLayout now owns useAssistantLifecycle and passes the resolved assistantId + assistantState to child routes via outlet context. This matches the platform's AssistantPageClient architecture where a single lifecycle owner wraps all views. - HomePageRoute no longer hardcodes assistantId='default' (which caused Django 404s since <uuid:assistant_id> can't match a string) - ChatPage consumes the layout-provided lifecycle instead of running its own duplicate instance - New useAssistantContext() hook wraps useOutletContext() with types - Fix stale VITE_AUTH_REQUIRED docs in AGENTS.md and CONVENTIONS.md (auth is always required; the env var gating was intentionally removed) Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
🤖 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:
|
| }); | ||
|
|
||
| const { assistantState, assistantId } = lifecycle; | ||
| const { assistantId, assistantState } = useAssistantContext(); |
There was a problem hiding this comment.
🚩 checkAssistant from context not wired to ChatRouteContent
The AssistantContextValue exposes checkAssistant (from the lifecycle hook), and ChatPage destructures the context at apps/web/src/domains/chat/chat-page.tsx:39. However, the chatRouteProps at line 216 still passes checkAssistant: noopVoid instead of the real function from context. This means onMaintenanceExited at apps/web/src/domains/chat/components/chat-route-content.tsx:1182 remains a no-op — when maintenance mode ends, the assistant status won't be automatically rechecked. This was already the case before this PR (the old code also passed noopVoid), but now that the lifecycle is properly lifted and exposed via context, it would be straightforward to wire checkAssistant from useAssistantContext() into the props.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch — wired checkAssistant from the context into chatRouteProps in d5028c0. onMaintenanceExited will now actually recheck assistant status.
There was a problem hiding this comment.
✦ APPROVE
Value: Fixes blank pages across all /assistant/* routes by correctly lifting useAssistantLifecycle from ChatPage (one leaf route) to ChatLayout (the shared parent), mirroring how AssistantPageClient owns the lifecycle in the platform repo.
What this does: Creates a typed outlet context (AssistantContextValue + useAssistantContext()) and passes resolved assistant state from ChatLayout to all child routes via <Outlet context={assistantContext} />. ChatPage and HomePageRoute now consume the shared context instead of each independently resolving (or hardcoding "default" for Home).
Per-file notes:
assistant-context.ts — Clean pattern. useOutletContext<AssistantContextValue>() is the correct RR7 data-mode idiom for typed layout-to-child state passing.
chat-layout.tsx — Lifecycle ownership is correct here. Both mobile and desktop <Outlet /> instances get the context. Two non-blocking observations:
lifecycle.autoGreetRefin theuseMemodep array is redundant — ref object identity is stable across renders (peruseRefcontract). Removing it won't change behavior, but it's a minor signal confusion.isRetired: falseandisNonProduction: falseare hardcoded. That's fine for now, but when those conditions become real, theonRedirect: navigatepath will now actually fire (previously the ChatPage-scoped lifecycle used a no-op navigate). Worth a TODO comment when that wires up.
chat-page.tsx — authLoading is kept from useAuthStore — presumably still used for its own render conditionals below the diff. Correct.
routes.tsx → HomePageRoute — Real assistantId replaces "default". This is the visible fix: Home page was blank because it was rendering against a fake ID with no assistant state behind it.
CONVENTIONS.md — Removing the "Auth is optional / VITE_AUTH_REQUIRED" section is a policy change worth flagging for awareness: local dev and self-hosting no longer have an opt-out path in the docs. Intentional hardening or docs-only cleanup for now? Either way, harmless to ship.
Anti-pattern checks: ✅ Zustand selectors use createSelectors pattern (useAuthStore.use.isLoggedIn(), .use.isLoading()). ✅ No useShallow on owned stores. ✅ Imports from react-router. ✅ No barrel files. ✅ No clientLoader/clientAction. Clean across the board.
Vellum Constitution — Trust-seeking: lifecycle state resolves once at the layout boundary and flows transparently to all child routes, eliminating the silent blank-page failure mode users were hitting.
Replace noopVoid with the real checkAssistant from useAssistantContext() so onMaintenanceExited actually rechecks assistant status. Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
d5028c0
Prompt / plan
All pages in the Vite web app (
/assistant/home,/assistant,/assistant/library,/assistant/settings) render blank or with placeholder content when runningvel up --exclude macos --vite.Root cause
The assistant lifecycle (
useAssistantLifecycle) was scoped toChatPage— one of several child routes — instead ofChatLayout, the shared layout wrapping all/assistant/*routes.In the platform repo,
AssistantPageClientacts as both layout and lifecycle owner — it resolves the real assistant UUID once and passes it to all child views. The Vite app's port placed the lifecycle insideChatPageonly, leaving other routes (home, library, settings) without access to the resolved assistant ID.HomePageRoutehardcodedassistantId="default"as a placeholder. Django's URL patternassistants/<uuid:assistant_id>/<path:rest>/can't match the string"default", so all API calls returned 404 — the home page rendered an empty greeting with no feed items, no avatar, and no suggestions.How the code got into this state
AssistantShellintoRootLayout+ChatLayout— but didn't move the lifecycle to the layout level.useAssistantLifecycleinsideChatPagerather thanChatLayout.assistantId="default"as a placeholder since the lifecycle wasn't available at that level.requiresAuth()/VITE_AUTH_REQUIREDgating (auth is always required) but left stale documentation inAGENTS.mdandCONVENTIONS.md.What changed
Lift
useAssistantLifecycletoChatLayout— the layout now owns the lifecycle and passes the resolvedassistantId+assistantStateto child routes via React Router v7's outlet context.New
useAssistantContext()hook — typed wrapper arounduseOutletContext()for child routes to consume the layout-provided lifecycle. Lives atsrc/domains/chat/assistant-context.ts.Fix
HomePageRoute— consumesuseAssistantContext()to get the real assistant UUID instead of hardcoding"default".Simplify
ChatPage— removes the duplicateuseAssistantLifecyclecall. ReadsassistantIdandassistantStatefrom the layout context.Wire
checkAssistantintoChatRouteContent— replaces the pre-existingnoopVoidplaceholder with the realcheckAssistantfrom the lifecycle context, soonMaintenanceExitedactually rechecks assistant status.Fix stale docs — removes misleading
VITE_AUTH_REQUIREDdocumentation fromAGENTS.mdandCONVENTIONS.md. Auth is always required; the env var gating was intentionally removed.Prevention
Added guidance to
AGENTS.mddocumenting the convention: assistant lifecycle is owned byChatLayoutand consumed viauseAssistantContext()— never hardcode or independently resolve the assistant ID in child routes.Alternatives considered and rejected
Just fix
HomePageRouteto calluseAssistantLifecycledirectly — rejected because it duplicates the lifecycle (each route runs independent hatching/polling). Wasteful and inconsistent with the platform architecture.Zustand store for assistant state — considered but rejected for this fix. The lifecycle hook has imperative side effects (hatching, polling, retry) that don't map cleanly to a store. Outlet context keeps the lifecycle as a hook in the layout, matching both the platform pattern and React Router conventions.
Test plan
bun run lint— passesbunx tsc --noEmit— passesLink to Devin session: https://app.devin.ai/sessions/1b39bae960a146a498035acc9bb663c6
Requested by: @ashleeradka