diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json
index ec9a32f35..7b03fc37e 100644
--- a/src/__tests__/baselines/source-function-lines.json
+++ b/src/__tests__/baselines/source-function-lines.json
@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
- "count": 338,
- "total": 109885,
+ "count": 337,
+ "total": 109677,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
@@ -99,7 +99,6 @@
"components/documents/sources/document-source-picker.tsx :: DocumentSourcePicker": 563,
"components/illness/log-day-sheet.tsx :: LogDaySheet": 265,
"components/illness/new-episode-sheet.tsx :: NewEpisodeSheet": 264,
- "components/insights/coach-panel/attachment-picker.tsx :: AttachmentPicker": 208,
"components/insights/coach-panel/chat-bubble.tsx :: ChatBubbleImpl": 419,
"components/insights/coach-panel/coach-conversation.tsx :: CoachConversation": 1002,
"components/insights/coach-panel/coach-input.tsx :: CoachInput": 259,
diff --git a/src/__tests__/client-fetch-limit-guard.test.ts b/src/__tests__/client-fetch-limit-guard.test.ts
index 514275f8b..13eae31a4 100644
--- a/src/__tests__/client-fetch-limit-guard.test.ts
+++ b/src/__tests__/client-fetch-limit-guard.test.ts
@@ -77,7 +77,7 @@ const ALLOWLIST: readonly Allowed[] = [
file: "components/insights/coach-panel/attachment-picker.tsx",
limit: 50,
reason:
- "Search-backed picker: the query narrows the whole document vault server-side.",
+ "Search-backed picker: the query narrows the whole document vault server-side, and a cut list says so and points to the search.",
},
{
file: "components/documents/document-picker-sheet.tsx",
diff --git a/src/components/insights/coach-panel/__tests__/attachment-picker-cap.test.tsx b/src/components/insights/coach-panel/__tests__/attachment-picker-cap.test.tsx
new file mode 100644
index 000000000..a4d117b05
--- /dev/null
+++ b/src/components/insights/coach-panel/__tests__/attachment-picker-cap.test.tsx
@@ -0,0 +1,71 @@
+/**
+ * The Coach's "Choose from documents" picker reads one page of the vault, the
+ * newest fifty. With a longer vault it listed those and said nothing, so an
+ * older report seemed not to exist unless one already knew to search for it.
+ *
+ * ResponsiveSheet portals its body, which static markup does not render, so it
+ * is mocked down to a wrapper that always shows its children.
+ */
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+const page = vi.hoisted(() => ({ nextCursor: null as string | null }));
+
+vi.mock("@/components/ui/responsive-sheet", () => ({
+ ResponsiveSheet: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+}));
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ data: {
+ // A cut page is a full one.
+ documents: Array.from({ length: page.nextCursor ? 50 : 3 }, (_, i) => ({
+ id: `d${i}`,
+ kind: "LAB_RESULT",
+ title: `Blood panel ${i}`,
+ filename: null,
+ hasContentIndex: true,
+ })),
+ nextCursor: page.nextCursor,
+ },
+ isPending: false,
+ isError: false,
+ }),
+}));
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { AttachmentPicker } from "../attachment-picker";
+
+function render() {
+ return renderToStaticMarkup(
+
+ undefined}
+ remainingSlots={3}
+ excludeIds={[]}
+ onConfirm={() => undefined}
+ />
+ ,
+ );
+}
+
+describe("AttachmentPicker", () => {
+ it("says the list is cut, and how to reach the rest", () => {
+ page.nextCursor = "d49";
+ const html = render();
+ expect(html).toContain('data-slot="coach-attachment-picker-capped"');
+ expect(html).toContain("Showing the newest 50");
+ });
+
+ it("says nothing when every document is listed", () => {
+ page.nextCursor = null;
+ const html = render();
+ expect(html).not.toContain('data-slot="coach-attachment-picker-capped"');
+ expect(
+ html.match(/data-slot="coach-attachment-picker-item"/g),
+ ).toHaveLength(3);
+ });
+});
diff --git a/src/components/insights/coach-panel/attachment-picker.tsx b/src/components/insights/coach-panel/attachment-picker.tsx
index e733d825f..b86fd9fa1 100644
--- a/src/components/insights/coach-panel/attachment-picker.tsx
+++ b/src/components/insights/coach-panel/attachment-picker.tsx
@@ -45,6 +45,58 @@ export interface AttachmentPickerProps {
onConfirm: (ids: string[]) => void;
}
+/** One document in the picker: a toggle, disabled until it is indexed. */
+function PickerRow({
+ doc,
+ isSelected,
+ disabled,
+ onToggle,
+}: {
+ doc: InboundDocumentDto;
+ isSelected: boolean;
+ disabled: boolean;
+ onToggle: () => void;
+}) {
+ const { t } = useTranslations();
+ const title = doc.title ?? doc.filename ?? t("documents.card.untitled");
+ const Icon = DOCUMENT_KIND_ICONS[doc.kind];
+ return (
+
+
+
+ );
+}
+
export function AttachmentPicker({
open,
onOpenChange,
@@ -107,6 +159,8 @@ export function AttachmentPicker({
);
const capReached = selected.length >= remainingSlots;
+ // More match than one page: say so, and that the search reaches the rest.
+ const cut = Boolean(list.data?.nextCursor);
function toggle(id: string) {
setSelected((prev) => {
@@ -200,55 +254,28 @@ export function AttachmentPicker({