From c6f47726069c6bf69bbb7419f499d8634e01843a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 21 Jun 2026 13:59:05 +0200 Subject: [PATCH] fix(coach): default to new chat and never dead-end on conversations Reverse the resume-most-recent entry default: opening the Coach now lands on the new-chat hero. The only exception is an unread coach-initiated message (nudge-status `unread`), which auto-opens the most-recent thread so the user sees what the Coach wrote. The `?c=` deep-link is unchanged. Add a dedicated, always-visible Conversations button (plus a New chat button) to the page surface so reaching past conversations no longer hides in the composer menu. The drawer's Conversations handoff now routes to `/coach?view=conversations`, which opens the history drawer on arrival, so the side-view to full-view path always shows the list instead of a blank pane. --- src/app/coach/page.tsx | 51 ++++++++++++++----- .../coach-conversation-deeplink.test.tsx | 21 ++++++++ .../coach-panel/coach-conversation.tsx | 48 ++++++++++++++++- .../insights/coach-panel/coach-drawer.tsx | 18 +++++-- 4 files changed, 120 insertions(+), 18 deletions(-) diff --git a/src/app/coach/page.tsx b/src/app/coach/page.tsx index 0274097d2..3e82f4059 100644 --- a/src/app/coach/page.tsx +++ b/src/app/coach/page.tsx @@ -2,11 +2,15 @@ import { Suspense, useEffect } from "react"; import { useRouter, useSearchParams } from "next/navigation"; +import { useQuery } from "@tanstack/react-query"; import { CoachConversation } from "@/components/insights/coach-panel/coach-conversation"; +import type { CoachNudgeStatus } from "@/components/insights/layout-coach-fab"; import { useCoachLaunch } from "@/lib/insights/coach-launch-context"; import { useFeatureFlags } from "@/hooks/use-feature-flags"; import { useDisableCoach } from "@/hooks/use-disable-coach"; +import { queryKeys } from "@/lib/query-keys"; +import { apiGet } from "@/lib/api/api-fetch"; /** * v1.12.0 (Coach v2 #6) — full-page Coach conversation. @@ -41,32 +45,53 @@ import { useDisableCoach } from "@/hooks/use-disable-coach"; * the active conversation. All of that lives in ``'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=` 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 ``'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. + * v1.19.1 (C1/C5) — the Coach now DEFAULTS to the new-chat hero, reversing + * the v1.19.0 (W7) "always resume most-recent" default the maintainer + * disliked. Resolution order on mount: + * - `?c=` → open that specific thread (explicit deep-link, unchanged). + * - `?c=new` or no param → the new-chat hero. + * - `?view=conversations` → land on the new-chat hero AND open the + * conversation-history drawer, so the drawer's "Conversations" handoff + * (C5) never drops the user on a blank pane. + * - EXCEPTION: when the Coach has proactively written an UNREAD message + * (`/api/insights/coach/nudge-status` → `unread`), auto-open the + * most-recent thread so the user lands on what the Coach said. + * 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. + // defaults to the new-chat hero. const rawC = searchParams.get("c"); const deepLinkedId = rawC && rawC !== "new" ? rawC : null; + // C5 — the drawer's "Conversations" handoff routes here with this flag so + // the page opens the history drawer on arrival instead of a blank hero. + const openConversations = searchParams.get("view") === "conversations"; + + // C1 exception — an unread coach-initiated message opens the most-recent + // conversation (which holds that proactive turn). Only consulted when the + // entry did not pin a specific thread or ask for a fresh chat / list. + const exceptionEligible = deepLinkedId === null && rawC !== "new"; + const { data: nudge } = useQuery({ + queryKey: queryKeys.coachNudgeStatus(), + queryFn: async (): Promise => + apiGet("/api/insights/coach/nudge-status"), + enabled: exceptionEligible && !openConversations, + staleTime: 5 * 60 * 1000, + }); + const hasUnreadCoachMessage = exceptionEligible && nudge?.unread === true; return ( ); } diff --git a/src/components/insights/coach-panel/__tests__/coach-conversation-deeplink.test.tsx b/src/components/insights/coach-panel/__tests__/coach-conversation-deeplink.test.tsx index fd9894a48..c7bbc9049 100644 --- a/src/components/insights/coach-panel/__tests__/coach-conversation-deeplink.test.tsx +++ b/src/components/insights/coach-panel/__tests__/coach-conversation-deeplink.test.tsx @@ -135,4 +135,25 @@ describe(" page deep-link (#67)", () => { // though recent conversations exist in the cache. expect(html).toContain('data-slot="coach-hero"'); }); + + // v1.19.1 (C2) — a dedicated, always-visible Conversations button on the + // page surface (not buried in the composer `+` menu). + it("renders the dedicated Conversations button on the page surface", () => { + const html = render(, makeClient()); + expect(html).toContain('data-slot="coach-page-conversations"'); + expect(html).toContain('data-slot="coach-page-new-chat"'); + }); + + // v1.19.1 (C5) — entering via the drawer handoff (`?view=conversations`) + // keeps the new-chat hero (no thread auto-resumed) while opening the + // history drawer; the toolbar + button stay present so the pane is never + // a blank dead-end. + it("keeps the hero and toolbar when opened with openHistoryOnMount", () => { + const html = render( + , + makeClient(), + ); + expect(html).toContain('data-slot="coach-hero"'); + expect(html).toContain('data-slot="coach-page-conversations"'); + }); }); diff --git a/src/components/insights/coach-panel/coach-conversation.tsx b/src/components/insights/coach-panel/coach-conversation.tsx index b166d6334..add693bda 100644 --- a/src/components/insights/coach-panel/coach-conversation.tsx +++ b/src/components/insights/coach-panel/coach-conversation.tsx @@ -3,7 +3,7 @@ import { useEffect, useReducer, useState } from "react"; import Link from "next/link"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Plus, Settings, Sparkles } from "lucide-react"; +import { MessagesSquare, Plus, Settings, Sparkles } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -137,6 +137,13 @@ export interface CoachConversationProps { * "new chat" or thread switch is never overridden. */ autoOpenMostRecent?: boolean; + /** + * v1.19.1 (C5) — page surface only: open the conversation-history drawer + * once on mount. The drawer's "Conversations" handoff routes to + * `/coach?view=conversations`, which sets this so the user always sees the + * conversation list on arrival instead of a blank new-chat pane. + */ + openHistoryOnMount?: boolean; } export function CoachConversation({ @@ -152,6 +159,7 @@ export function CoachConversation({ surface, initialConversationId, autoOpenMostRecent = false, + openHistoryOnMount = false, }: CoachConversationProps) { const { t } = useTranslations(); @@ -165,7 +173,12 @@ export function CoachConversation({ // the composer's `+` actions menu. Replaces the old inline collapsible // rail + top rail-tray strip; the top header bar is gone entirely on the // page so the composer is the single control hub. - const [historyDrawerOpen, setHistoryDrawerOpen] = useState(false); + // v1.19.1 (C5) — seed the history drawer open when the page is entered via + // the drawer's "Conversations" handoff (`?view=conversations`), so the + // list is on screen immediately rather than a blank hero. + const [historyDrawerOpen, setHistoryDrawerOpen] = useState( + () => openHistoryOnMount, + ); const [inputValue, setInputValue] = useResettableValue(prefill ?? ""); // v1.16.4 — self-context backflow: `pendingAdopt` raises a quiet // offer to fold a clarifying-question answer back into the @@ -484,6 +497,37 @@ export function CoachConversation({ data-variant={surface} className={cn("flex min-h-0 flex-1 flex-col", className)} > + {/* v1.19.1 (C2) — a clear, always-visible "Conversations" button on + the page surface. The composer's `+` menu still carries the same + action, but the maintainer wanted an obvious, dedicated affordance + to reach past conversations rather than one buried in a menu. */} +
+ + +
{heroActive ? ( ) : ( diff --git a/src/components/insights/coach-panel/coach-drawer.tsx b/src/components/insights/coach-panel/coach-drawer.tsx index 07885095e..bcbfce7d3 100644 --- a/src/components/insights/coach-panel/coach-drawer.tsx +++ b/src/components/insights/coach-panel/coach-drawer.tsx @@ -88,6 +88,17 @@ export function CoachDrawer({ open, onOpenChange, prefill }: CoachDrawerProps) { router.push("/coach"); }, [onOpenChange, router]); + // v1.19.1 (C5) — the drawer's "Conversations" affordance hands off to the + // full page WITH the `?view=conversations` flag so the page opens the + // conversation-history drawer on arrival. Previously it shared + // `handleMaximize` and, after the v1.19.1 new-chat default (C1), landed on + // a blank hero with no list — the dead-end the maintainer reported. + const handleOpenConversations = useCallback(() => { + onOpenChange(false); + resetRef.current?.(); + router.push("/coach?view=conversations"); + }, [onOpenChange, router]); + return (