fix(coach): drop the search field from the conversations panel

The panel lists the history grouped by recency with load-more, rename
and delete; title search stays on the full /coach/conversations page.
This commit is contained in:
Marc-André Bombeck committed 2026-10-05 21:05:13 +02:00
1 parent 6cea4be2a1
commit b6ce54e2c6
5 files changed
+18 -40

No files matched your search

+5 -2
View File
@@ -472,7 +472,7 @@ test.describe("Coach page frame", () => {
expect(Math.abs(closed.composer - open.composer)).toBeLessThanOrEqual(1);
// Closed, nothing inside the panel is reachable.
await expect(
panel(page).locator('[data-slot="coach-history-search"]'),
panel(page).locator('[data-slot="coach-history-select"]').first(),
).not.toBeInViewport();
await expectNoSidewaysScroll(page);
await shot(page, testInfo, `coach-frame-${width}-closed`);
@@ -484,7 +484,10 @@ test.describe("Coach page frame", () => {
// Open again, then Escape from inside closes it, focus to the toggle.
await toggle(page).click();
await expect(panel(page)).toHaveAttribute("data-state", "open");
await panel(page).locator('[data-slot="coach-history-search"]').focus();
await panel(page)
.locator('[data-slot="coach-history-select"]')
.first()
.focus();
await page.keyboard.press("Escape");
await expect(panel(page)).toHaveAttribute("data-state", "closed");
await expect(toggle(page)).toBeFocused();
@@ -141,15 +141,15 @@ describe("<HistoryRail>", () => {
expect(html).toContain("Your conversations will appear here.");
});
it("mounts the search input with the localised placeholder", () => {
it("carries no search field (search lives on the full conversations page)", () => {
const client = makeClientWithConversations(samplePage);
const html = render(
<HistoryRail activeId={null} onSelect={() => {}} />,
client,
);
expect(html).toMatch(
/data-slot="coach-history-search"[^>]*placeholder="Search conversations…"/,
);
expect(html).toContain('data-slot="coach-history-list"');
expect(html).not.toContain('type="search"');
expect(html).not.toContain("Search conversations");
});
it("uses German strings when locale is 'de'", () => {
@@ -176,7 +176,7 @@ export function CoachDrawer({
// v1.21.4 (Coach-UI B) — the drawer's "Conversations" affordance now hands
// off to the dedicated conversation-history page (`/coach/conversations`),
// which renders the search + recency-grouped list as a sibling of the
// which renders the recency-grouped list as a sibling of the
// Coach page. The former `?view=conversations` in-page slide-in drawer is
// gone; selecting a row there routes back to `/coach?c=<id>`.
const handleOpenConversations = useCallback(() => {
@@ -366,9 +366,8 @@ export function ConversationsPanel({
data-slot="coach-conversations-panel"
className="w-[22rem] max-w-[90vw] gap-0 p-0 sm:max-w-[22rem]"
onOpenAutoFocus={(event) => {
// Land on the sheet itself, not its first control: on a phone
// focusing the search field would raise the keyboard over the
// list the person opened the sheet to read.
// Land on the sheet itself, not its first control, so opening
// the sheet does not paint a focus ring on a header button.
event.preventDefault();
(event.currentTarget as HTMLElement | null)?.focus();
}}
@@ -8,7 +8,6 @@ import {
Paperclip,
Pencil,
RotateCw,
Search,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
@@ -20,10 +19,8 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useLoadMoreSentinel } from "@/hooks/use-load-more-sentinel";
import type { CoachConversationDTO } from "@/lib/ai/coach/types";
import { groupConversationsByRecency } from "@/lib/insights/coach-conversation-groups";
@@ -36,14 +33,14 @@ import {
} from "./use-coach";
/**
* The Coach's conversation list: search, the full history grouped by recency
* The Coach's conversation list: the full history grouped by recency
* (Today / Yesterday / This week / Earlier), and one row per conversation with
* a "⋯" menu for Rename and Delete.
*
* It fills the Coach page's conversations panel and the drawer's history
* tray. The history is cursor-paginated (`useInfiniteQuery`, loaded as the
* list scrolls near its end) and searched server-side on the title, debounced
* 200 ms.
* list scrolls near its end). Title search lives on the standalone
* `/coach/conversations` page, not here.
*
* Rename edits the title in place (`ConversationRename`, Enter saves, Escape
* cancels without closing the panel around it) and returns focus to the row's
@@ -88,8 +85,6 @@ export function HistoryRail({
onUndoDeleteActive,
}: HistoryRailProps) {
const { t } = useTranslations();
const [filter, setFilter] = useState<string>("");
const debouncedFilter = useDebouncedValue(filter, 200);
const {
conversations,
isLoading,
@@ -98,19 +93,17 @@ export function HistoryRail({
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useCoachConversationHistory({ search: debouncedFilter });
} = useCoachConversationHistory();
const ownDeletion = useDeleteCoachConversationWithUndo();
const { pendingDeleteIds, requestDelete, undoDelete } =
deletion ?? ownDeletion;
// `pendingDeleteIds` hides rows inside the undo window; the server-side
// search already resolved everything else.
// `pendingDeleteIds` hides rows inside the undo window.
const visible = useMemo(
() => conversations.filter((c) => !pendingDeleteIds.has(c.id)),
[conversations, pendingDeleteIds],
);
const groups = useMemo(() => groupConversationsByRecency(visible), [visible]);
const isSearching = debouncedFilter.trim().length > 0;
// A `useState`-backed callback ref so the sentinel hook re-runs once the
// scroll container exists (null on the very first render).
@@ -153,21 +146,6 @@ export function HistoryRail({
{t("insights.coach.historyTitle")}
</h3>
)}
<div className="relative w-full">
<Search
aria-hidden="true"
className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2"
/>
<Input
type="search"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder={t("insights.coach.historySearchPlaceholder")}
aria-label={t("insights.coach.historySearchPlaceholder")}
data-slot="coach-history-search"
className="h-10 w-full pl-9"
/>
</div>
<div
ref={setListNode}
data-slot="coach-history-list"
@@ -202,9 +180,7 @@ export function HistoryRail({
data-slot="coach-history-empty"
className="text-muted-foreground px-2 py-3 text-sm leading-relaxed"
>
{isSearching
? t("insights.coach.historySearchEmpty")
: t("insights.coach.historyEmpty")}
{t("insights.coach.historyEmpty")}
</p>
) : (
groups.map((group) => (