feat(coach): deep-link the Coach page into a conversation

The Coach page opened a blank new chat regardless of where the user came
from. Make it a deep-link target: a `?c=<id>` query param opens that
thread on mount, and absent it the page auto-opens the user's most-recent
conversation. "Most recent" is the `updatedAt desc` head of the rail
list, so the same thread opens on web and mobile.

The shared conversation surface grows two additive props
(`initialConversationId`, `autoOpenMostRecent`) that feed its existing
selection state; the history drawer's selection stays the single source of
truth. The drawer surface is unchanged — both props default off and the
rail list is only fetched when auto-open is requested.
This commit is contained in:
Marc-André Bombeck committed 2026-06-21 00:25:54 +02:00
1 parent 6975849bec
commit ead44e79d9
3 files changed
+224 -5

No files matched your search

+37 -3
View File
@@ -1,7 +1,7 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { Suspense, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { CoachConversation } from "@/components/insights/coach-panel/coach-conversation";
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
@@ -40,7 +40,37 @@ import { useDisableCoach } from "@/hooks/use-disable-coach";
* constant, centred, max-width-capped column across the new-chat hero and
* the active conversation. All of that lives in `<CoachConversation>`'s
* page branch, so the page stays a thin gating + sizing wrapper.
*
* v1.18.11 (W11, #67) — the page is now a deep-link target INTO a
* conversation. A `?c=<id>` query param opens that specific thread on
* mount; absent it, the page auto-opens the user's most-recent thread
* (server-authoritative `updatedAt desc` head of the rail, so it lands the
* same thread on web + mobile). Both feed `<CoachConversation>`'s existing
* selection state — the dashboard Coach entry navigates straight into the
* live conversation instead of a blank landing. `?c=new` (or no thread at
* all) keeps the new-chat hero. The search-param read sits in a Suspense
* child so the client-bailout never de-opts the route.
*/
function CoachPageBody() {
const searchParams = useSearchParams();
// `?c=new` is an explicit "start a fresh chat" escape hatch; any other
// value is treated as a conversation id to open. A blank/absent param
// falls through to most-recent auto-open.
const rawC = searchParams.get("c");
const deepLinkedId = rawC && rawC !== "new" ? rawC : null;
return (
<CoachConversation
surface="page"
autoFocusComposer
initialConversationId={deepLinkedId}
// Only auto-resolve most-recent when the entry did not pin a thread
// and did not ask for a fresh chat.
autoOpenMostRecent={deepLinkedId === null && rawC !== "new"}
/>
);
}
export default function CoachPage() {
const router = useRouter();
const launch = useCoachLaunch();
@@ -88,7 +118,11 @@ export default function CoachPage() {
// is hidden, so the page reclaims the full height below the TopBar.
className="bg-background -mx-4 -mt-6 -mb-20 flex h-[calc(100dvh-8rem-env(safe-area-inset-bottom,0px))] min-h-[32rem] flex-col overflow-hidden md:-mx-6 md:h-[calc(100dvh-4rem)]"
>
<CoachConversation surface="page" autoFocusComposer />
{/* `useSearchParams` requires a Suspense boundary so the client-search
bailout never opts the whole route out of static optimisation. */}
<Suspense fallback={null}>
<CoachPageBody />
</Suspense>
</div>
);
}