diff --git a/apps/web/src/shell/workbench-list.test.ts b/apps/web/src/shell/workbench-list.test.ts index fa0723465..1f0818d82 100644 --- a/apps/web/src/shell/workbench-list.test.ts +++ b/apps/web/src/shell/workbench-list.test.ts @@ -2,7 +2,9 @@ import { describe, expect, test } from "bun:test"; import type { Workbench } from "@corbits/chat-ui"; +import type { SidebarRow } from "./sidebar-rows"; import { + filterSidebarRows, orderWorkbenchRows, renamePayload, rowMenuLabels, @@ -20,6 +22,10 @@ function workbench(overrides: Partial = {}): Workbench { } as Workbench; } +function row(overrides: Partial = {}): SidebarRow { + return { kind: "workbench", workbench: workbench(overrides) }; +} + describe("rowMenuLabels", () => { test("offers Pin for an unpinned row and Unpin for a pinned one", () => { expect(rowMenuLabels({ pinned: false })).toEqual(["Rename", "Pin"]); @@ -112,3 +118,71 @@ describe("orderWorkbenchRows", () => { ]); }); }); + +describe("filterSidebarRows", () => { + // CL-6662: the row shows title + preview; search must match either, or a + // query visible in the preview (e.g. "Solvora") falsely returns no matches. + test("matches the displayed title", () => { + const rows = [ + row({ id: "ch_a", title: "Launch plan" }), + row({ id: "ch_b", title: "Research brief" }), + ]; + expect( + filterSidebarRows(rows, "launch").map((r) => r.workbench.id), + ).toEqual(["ch_a"]); + }); + + test("matches preview text even when the title does not (CL-6662)", () => { + const rows = [ + row({ + id: "ch_solvora", + title: "Myra", + preview: "Drafted the Solvora outreach email", + }), + row({ + id: "ch_other", + title: "Scout", + preview: "Weekly digest ready", + }), + ]; + expect( + filterSidebarRows(rows, "Solvora").map((r) => r.workbench.id), + ).toEqual(["ch_solvora"]); + }); + + test("is case-insensitive across title and preview", () => { + const rows = [ + row({ id: "ch_1", title: "Myra", preview: "Talked about Acme pricing" }), + ]; + expect(filterSidebarRows(rows, "ACME").map((r) => r.workbench.id)).toEqual([ + "ch_1", + ]); + expect(filterSidebarRows(rows, "myra").map((r) => r.workbench.id)).toEqual([ + "ch_1", + ]); + }); + + test("an empty or whitespace-only query returns every row", () => { + const rows = [row({ id: "ch_a" }), row({ id: "ch_b", title: "Other" })]; + expect(filterSidebarRows(rows, "").map((r) => r.workbench.id)).toEqual([ + "ch_a", + "ch_b", + ]); + expect(filterSidebarRows(rows, " ").map((r) => r.workbench.id)).toEqual([ + "ch_a", + "ch_b", + ]); + }); + + test("returns no rows when neither title nor preview matches", () => { + const rows = [row({ id: "ch_1", title: "Myra", preview: "Hello there" })]; + expect(filterSidebarRows(rows, "zzz")).toEqual([]); + }); + + test("a missing preview still matches on title alone", () => { + const rows = [row({ id: "ch_1", title: "Launch plan" })]; + expect( + filterSidebarRows(rows, "launch").map((r) => r.workbench.id), + ).toEqual(["ch_1"]); + }); +}); diff --git a/apps/web/src/shell/workbench-list.tsx b/apps/web/src/shell/workbench-list.tsx index 9c86ea1e9..d60ddfee2 100644 --- a/apps/web/src/shell/workbench-list.tsx +++ b/apps/web/src/shell/workbench-list.tsx @@ -178,6 +178,28 @@ export function orderWorkbenchRows( ]; } +/** + * Sidebar search over what the row actually shows: the displayed title and + * the preview snippet. Title-only matching left people staring at a visible + * preview word (CL-6662) and a "No matches" empty state. Case-insensitive + * substring; empty/whitespace query keeps every row. + */ +export function filterSidebarRows( + rows: readonly SidebarRow[], + query: string, +): readonly SidebarRow[] { + const needle = query.trim().toLowerCase(); + if (needle === "") return rows; + return rows.filter((row) => { + const title = + displayWorkbenchTitle(row.workbench.title, row.workbench.id) || + CHAT_STRINGS.unnamedWorkbench; + if (title.toLowerCase().includes(needle)) return true; + const preview = row.workbench.preview ?? ""; + return preview.toLowerCase().includes(needle); + }); +} + /** * One workbench row — avatar, name (the agent's for an agent conversation, * the row's own title for a multi-party one), optional shared/live, optional @@ -412,15 +434,7 @@ export function WorkbenchList({ ); } - const rowName = (row: SidebarRow): string => - displayWorkbenchTitle(row.workbench.title, row.workbench.id) || - CHAT_STRINGS.unnamedWorkbench; - - const q = query.trim().toLowerCase(); - const filtered = - q === "" - ? all - : all.filter((row) => rowName(row).toLowerCase().includes(q)); + const filtered = filterSidebarRows(all, query); const tenantId = selectedTenantId ?? "";