diff --git a/src/components/chat-new-dashboard.test.ts b/src/components/chat-new-dashboard.test.ts index b2b095167..2ba6c07c1 100644 --- a/src/components/chat-new-dashboard.test.ts +++ b/src/components/chat-new-dashboard.test.ts @@ -23,6 +23,7 @@ import { readFile } from "node:fs/promises"; const dash = await readFile(new URL("./chat-new-dashboard.tsx", import.meta.url), "utf8"); const chatView = await readFile(new URL("./chat-view.tsx", import.meta.url), "utf8"); const homeComposer = await readFile(new URL("./home-composer.tsx", import.meta.url), "utf8"); +const boardHook = await readFile(new URL("./home/use-dashboard-board.ts", import.meta.url), "utf8"); const css = await readFile(new URL("../styles/home-dashboard.css", import.meta.url), "utf8"); // ── (1) chat-view's empty-state split ──────────────────────────────────── @@ -118,6 +119,54 @@ assert.match( assert.match(dash, /const boardCards = useDashboardBoard\(\)/, "the board reads the live Tasks board"); assert.match(dash, /fetch\("\/api\/inbox", \{ cache: "no-store"/, "the needs-you tier reads the live inbox"); assert.match(dash, /groupInboxFeed\(items\)\.needsYou/, "needs-you uses the same attention tier as the bell"); +assert.match( + boardHook, + /familiarId: string \| null/, + "the lean dashboard-card projection must retain familiar ownership", +); +assert.match( + boardHook, + /familiarId: c\.familiarId \?\? null/, + "the Board response must carry familiar ownership into the dashboard model", +); +assert.match( + dash, + /filterFamiliarOwned\(boardCards,\s*familiar\.id\)/, + "Board open work must scope to the selected familiar before row derivation", +); +assert.match( + dash, + /filterFamiliarOwned\(needsYou,\s*familiar\.id\)/, + "Needs-you inbox work must scope to the selected familiar before row derivation", +); +assert.match( + dash, + /openWorkRows\(scopedBoardCards\)/, + "open-work counts, caps, and empty states must derive from scoped Board cards", +); +assert.match( + dash, + /scopedNeedsYou\.map/, + "open-work counts, caps, and empty states must derive from scoped Needs-you items", +); +assert.match( + dash, + /workFilter === "inbox" && scopedNeedsYou\.length > 0/, + "the Needs-you section link must use the familiar-scoped count", +); +assert.match( + dash, + /filterVisibleChatSessions\(sessions,\s*familiar\.id\)[\s\S]{0,300}?\.slice\(0,\s*RECENT_THREADS_CAP\)/, + "Recent threads must apply shared familiar visibility before the three-row cap", +); +assert.doesNotMatch( + dash.slice( + dash.indexOf("const recentThreads = useMemo"), + dash.indexOf("return (", dash.indexOf("const recentThreads = useMemo")), + ), + /\.sort\(/, + "Recent threads preserve the shared helper's canonical newest-first ordering", +); assert.match(dash, /OPEN_WORK_FILTERS\.map/, "the board renders the trimmed filter tabs"); assert.match( dash, diff --git a/src/components/chat-new-dashboard.tsx b/src/components/chat-new-dashboard.tsx index 92ad7dc99..b96144c21 100644 --- a/src/components/chat-new-dashboard.tsx +++ b/src/components/chat-new-dashboard.tsx @@ -29,10 +29,12 @@ import { Icon } from "@/lib/icon"; import { groupInboxFeed } from "@/lib/inbox-feed"; import { greetingForHour } from "@/lib/home-greeting"; import { relativeAge } from "@/lib/rss"; +import { filterVisibleChatSessions } from "@/lib/chat-projects"; import { useDashboardBoard } from "@/components/home/use-dashboard-board"; import { OPEN_WORK_FILTERS, OPEN_WORK_FILTER_LABEL, + filterFamiliarOwned, filterOpenWork, openWorkCounts, openWorkPriorityLabel, @@ -126,12 +128,20 @@ export function ChatNewDashboard({ // that item's session (or land on Schedules), read-stamped like the bell. const boardCards = useDashboardBoard(); const needsYou = useNeedsYou(); + const scopedBoardCards = useMemo( + () => filterFamiliarOwned(boardCards, familiar.id), + [boardCards, familiar.id], + ); + const scopedNeedsYou = useMemo( + () => filterFamiliarOwned(needsYou, familiar.id), + [needsYou, familiar.id], + ); const openWork = useMemo(() => { - const board = openWorkRows(boardCards).map((r) => ({ + const board = openWorkRows(scopedBoardCards).map((r) => ({ ...r, onOpen: () => navigateMode("board"), })); - const needs = needsYou.map((item) => ({ + const needs = scopedNeedsYou.map((item) => ({ id: `needs-${item.id}`, title: item.title, kind: "inbox" as const, @@ -148,7 +158,7 @@ export function ChatNewDashboard({ }, })); return [...board, ...needs]; - }, [boardCards, needsYou]); + }, [scopedBoardCards, scopedNeedsYou]); const [workFilter, setWorkFilter] = useState("all"); const workCounts = useMemo(() => openWorkCounts(openWork), [openWork]); // Capped so the no-scroll board fits the pane; "View all in Tasks →" carries @@ -157,13 +167,13 @@ export function ChatNewDashboard({ () => filterOpenWork(openWork, workFilter).slice(0, OPEN_WORK_ROWS_CAP), [openWork, workFilter], ); - // Recent threads: the freshest titled sessions, newest-first, capped. + // Recent threads: this familiar's freshest titled sessions, newest-first, + // capped after applying the shared chat visibility contract. const recentThreads = useMemo(() => { - return sessions - .filter((s) => !s.archived_at && !s.generated && Boolean(s.title?.trim())) - .sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")) + return filterVisibleChatSessions(sessions, familiar.id) + .filter((s) => Boolean(s.title?.trim())) .slice(0, RECENT_THREADS_CAP); - }, [sessions]); + }, [sessions, familiar.id]); return (
@@ -213,7 +223,7 @@ export function ChatNewDashboard({
Open work
- {workFilter === "inbox" && needsYou.length > 0 ? ( + {workFilter === "inbox" && scopedNeedsYou.length > 0 ? (