fix(coach): name the attached document(s) in the fenced banner; keep the attach menu within its popover

The fenced-conversation banner said a generic "N documents attached", which
gave no hint what a thread was about when returning to it later. It now names
the attached document(s): one document shows its title, two show both, three or
more show the first two plus an "and N more" tail. Reuses the existing
"Chatting about: …" doc-scope phrasing and the attachment DTO titles; the title
truncates within the pill so a long name never breaks the banner. Falls back to
a plain count only when no title has resolved yet.

Also constrain the composer attach menu: a long localised label — e.g. "Neues
Dokument hochladen" — could run past the popover edge. The menu items now shrink
and truncate within the menu width, mirroring the vault picker items.

Adds a "chattingAboutMore" string across all six locales.
This commit is contained in:
Marc-André Bombeck committed 2026-07-17 11:02:47 +02:00
1 parent 65f90160c7
commit f6e6593bcb
9 files changed
+45 -16

No files matched your search

+1
View File
@@ -2184,6 +2184,7 @@
"docScope": {
"badge": "Dokument",
"chattingAbout": "Chat über: {title}",
"chattingAboutMore": "Chat über: {titles} und {count} weitere",
"notIndexedHint": "Lies dieses Dokument zuerst im Archiv mit KI ein, damit der Coach dazu antworten kann."
},
"actionsMenu": "Aktionen",
+1
View File
@@ -2184,6 +2184,7 @@
"docScope": {
"badge": "Document",
"chattingAbout": "Chatting about: {title}",
"chattingAboutMore": "Chatting about: {titles} and {count} more",
"notIndexedHint": "Read this document with AI first in the vault so the Coach can answer about it."
},
"actionsMenu": "Actions",
+1
View File
@@ -2184,6 +2184,7 @@
"docScope": {
"badge": "Documento",
"chattingAbout": "Hablando sobre: {title}",
"chattingAboutMore": "Hablando sobre: {titles} y {count} más",
"notIndexedHint": "Primero lee este documento con IA en el archivo para que el Coach pueda responder sobre él."
},
"actionsMenu": "Acciones",
+1
View File
@@ -2184,6 +2184,7 @@
"docScope": {
"badge": "Document",
"chattingAbout": "Discussion sur : {title}",
"chattingAboutMore": "Discussion sur : {titles} et {count} de plus",
"notIndexedHint": "Lis d'abord ce document avec l'IA dans le coffre pour que le Coach puisse y répondre."
},
"actionsMenu": "Actions",
+1
View File
@@ -2184,6 +2184,7 @@
"docScope": {
"badge": "Documento",
"chattingAbout": "Stai parlando di: {title}",
"chattingAboutMore": "Stai parlando di: {titles} e altri {count}",
"notIndexedHint": "Leggi prima questo documento con l'IA nell'archivio così il Coach può rispondere."
},
"actionsMenu": "Azioni",
+1
View File
@@ -2184,6 +2184,7 @@
"docScope": {
"badge": "Dokument",
"chattingAbout": "Rozmowa o: {title}",
"chattingAboutMore": "Rozmowa o: {titles} i {count} więcej",
"notIndexedHint": "Najpierw wczytaj ten dokument z pomocą AI w archiwum, aby Coach mógł na jego temat odpowiadać."
},
"actionsMenu": "Działania",
@@ -18,7 +18,7 @@ import { CoachConversation } from "../coach-conversation";
* When the Coach is fenced — here seeded via the `/coach?doc=<id>` hand-off,
* which stages ONE document as a pending first-turn attachment before any
* thread exists — the shared `<CoachConversation>` surface paints the
* `coach-doc-scope` banner: a paperclip + "N documents attached" count and the
* `coach-doc-scope` banner: a paperclip + the attached document name(s) and the
* honest fencing line (the coach runs WITHOUT access to health data and reads
* only the attached documents). While a staged attachment is still being
* content-indexed the banner adds the "still being indexed" hint. On a normal
@@ -73,8 +73,8 @@ describe("<CoachConversation> fenced banner (S7)", () => {
makeClient(true),
);
expect(html).toContain('data-slot="coach-doc-scope"');
// Exactly one document staged → the singular count.
expect(html).toContain("1 document attached");
// Exactly one document staged → the banner names it, not a generic count.
expect(html).toContain(`Chatting about: ${TITLE}`);
// The honest fencing line is always present on a fenced conversation.
expect(html).toContain(FENCING_LINE);
// Indexed → no "still being indexed" hint.
@@ -954,20 +954,35 @@ export function CoachConversation({
// ONLY the attached documents, and counts the attachments. Rendered in BOTH
// surfaces (page + drawer) above the thread so the fence is always visible.
// Null on a normal health thread.
const attachedCount = attachmentPills.length;
// v1.29.1 — name the attached document(s) in the banner so returning to a
// fenced thread later is clear about what it is about, instead of a generic
// "N documents attached". One doc → its title; two → both, comma-joined
// through the reused `docScope.chattingAbout` phrasing; three or more → the
// first two plus an "and N more" tail. Falls back to the plain count only when
// no title has resolved yet (a sticky-fenced thread whose attachment is still
// in flight), so the banner is never empty.
const attachedTitles = attachmentPills.map((pill) => pill.title);
const attachedCount = attachedTitles.length;
const docScopeText =
attachedCount === 0
? t("insights.coach.attach.documentsAttachedOther", { count: 0 })
: attachedCount <= 2
? t("insights.coach.docScope.chattingAbout", {
title: attachedTitles.join(", "),
})
: t("insights.coach.docScope.chattingAboutMore", {
titles: attachedTitles.slice(0, 2).join(", "),
count: attachedCount - 2,
});
const docScopeBanner = fenced ? (
<div
data-slot="coach-doc-scope"
className="border-border/70 bg-muted/20 flex shrink-0 flex-col gap-1 border-b px-4 py-2 sm:px-6"
>
<div className="flex min-w-0 items-center gap-2 text-xs">
<span className="bg-primary/15 text-primary inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 font-medium">
<Paperclip className="size-3" aria-hidden="true" />
{attachedCount === 1
? t("insights.coach.attach.documentsAttachedOne")
: t("insights.coach.attach.documentsAttachedOther", {
count: attachedCount,
})}
<span className="bg-primary/15 text-primary inline-flex min-w-0 items-center gap-1 rounded-full px-2 py-0.5 font-medium">
<Paperclip className="size-3 shrink-0" aria-hidden="true" />
<span className="truncate">{docScopeText}</span>
</span>
</div>
<p className="text-muted-foreground text-[11px] leading-snug">
@@ -595,13 +595,19 @@ export function CoachInput({
<Paperclip className="size-5 sm:size-4" aria-hidden="true" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" side="top" className="w-56">
{/* v1.29.1 — the item labels shrink + truncate within the popover width
(`min-w-0` + a `truncate` span) so a long localised label — e.g.
"Neues Dokument hochladen" — can never overflow past the menu edge.
Mirrors the vault picker items' width handling. */}
<DropdownMenuContent align="start" side="top" className="w-60">
<DropdownMenuItem
data-slot="coach-input-attach-vault"
onSelect={() => onPickFromVault?.()}
>
<FolderOpen className="size-4" aria-hidden="true" />
{t("insights.coach.attach.menuChooseFromDocuments")}
<FolderOpen className="size-4 shrink-0" aria-hidden="true" />
<span className="min-w-0 flex-1 truncate">
{t("insights.coach.attach.menuChooseFromDocuments")}
</span>
</DropdownMenuItem>
<DropdownMenuItem
data-slot="coach-input-attach-upload"
@@ -611,8 +617,10 @@ export function CoachInput({
setTimeout(() => attachFileInputRef.current?.click(), 0)
}
>
<Upload className="size-4" aria-hidden="true" />
{t("insights.coach.attach.menuUploadNew")}
<Upload className="size-4 shrink-0" aria-hidden="true" />
<span className="min-w-0 flex-1 truncate">
{t("insights.coach.attach.menuUploadNew")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>