From 54f0d271dde5b185a8d33a52ab378627ed89d7da Mon Sep 17 00:00:00 2001 From: louispaulb Date: Mon, 15 Jun 2026 08:09:07 -0400 Subject: [PATCH] feat(inbox): global thread scroll + close (X) on another agent's draft mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Email posts now render at FULL content height (fitFrame cap raised 1800→20000 guard) so there is no per-message internal scroll — the whole thread scrolls as one. - The live draft mirror ("X rédige une réponse…") gets a close X (closeOtherDraft): dismiss another agent's in-progress draft preview; reset on conversation switch so a genuinely new draft re-appears. Verified: a 60-paragraph email renders at 2681px (no inner scrollbar); the mirror shows a close button that hides it on click. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/ops/src/components/shared/ConversationPanel.vue | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/ops/src/components/shared/ConversationPanel.vue b/apps/ops/src/components/shared/ConversationPanel.vue index 4a6dd95..5b26fda 100644 --- a/apps/ops/src/components/shared/ConversationPanel.vue +++ b/apps/ops/src/components/shared/ConversationPanel.vue @@ -383,7 +383,7 @@
-
{{ shortAgent(otherDraft.agent) }} rédige une réponse en direct…
+
{{ shortAgent(otherDraft.agent) }} rédige une réponse en direct…Fermer cet aperçu
@@ -974,7 +974,9 @@ const readers = computed(() => { return Object.keys(rb).filter(e => e && e !== meEmail.value).map(e => ({ email: e, ...rb[e] })).sort((a, b) => String(b.readAt || '').localeCompare(String(a.readAt || ''))) }) // Brouillon d'un AUTRE agent sur cette conversation (miroir temps réel ; null si c'est le mien ou aucun). -const otherDraft = computed(() => sharedDraft.value[coordToken.value] || null) // handleConvDraft a déjà retiré mon propre onglet (filtre sid) +const closedDrafts = ref(new Set()) // miroirs fermés manuellement via le X (réaffichés au changement de fil) +const otherDraft = computed(() => (closedDrafts.value.has(coordToken.value) ? null : (sharedDraft.value[coordToken.value] || null))) // handleConvDraft a déjà retiré mon propre onglet (filtre sid) +function closeOtherDraft () { const s = new Set(closedDrafts.value); s.add(coordToken.value); closedDrafts.value = s } const coordQueue = computed(() => activeConv.value?.queue || activeDiscussion.value?.queue || '') // Signature SUGGÉRÉE selon la FILE (département) du fil : la réponse pré-écrite dont mailbox === file. One-tap, jamais forcée (le brouillon est partagé → ne pas écraser/diffuser une surprise). const suggestedSignature = computed(() => { if (!isEmailConv.value) return null; const q = coordQueue.value; return q ? (cannedList.value.find(c => c.html && c.mailbox === q) || null) : null }) @@ -998,7 +1000,8 @@ function openReply () { replyOpen.value = true; if (coordToken.value) notifyTypi // Ajuste un iframe courriel à SON contenu (fini le 300px fixe / les blancs). Sûr : sandbox `allow-same-origin` SANS `allow-scripts` → le parent lit le DOM pour mesurer, aucun script du courriel ne s'exécute. function fitFrame (f) { if (!f) return - const measure = () => { try { const h = f.contentDocument.body.scrollHeight; if (h > 4) f.style.height = Math.min(Math.max(h + 8, 48), 1800) + 'px' } catch (err) { f.style.height = '300px' } } + // Hauteur = TOUT le contenu (pas de plafond → aucun scroll interne au message ; c'est le fil entier qui défile globalement). Garde-fou 20000px contre une valeur aberrante. + const measure = () => { try { const h = f.contentDocument.body.scrollHeight; if (h > 4) f.style.height = Math.min(Math.max(h + 8, 48), 20000) + 'px' } catch (err) { f.style.height = '300px' } } measure(); setTimeout(measure, 80) // @load (ou un déplié) peut précéder la mise en page → 2e mesure après stabilisation } function fitEmailFrame (e) { fitFrame(e.target) } @@ -1076,7 +1079,7 @@ function msgSnippet (m) { const t = String(m.text || '').replace(/^✉️[^\n]*\ function cleanBody (t) { return String(t || '').replace(/^✉️[^\n]*\n+/, '') } // retire l'ancien préfixe sujet collé au corps (double en-tête) const lastMessageId = computed(() => { const ms = activeDiscussion.value?.messages || []; return ms.length ? ms[ms.length - 1].id : null }) function expandLast () { const ms = activeDiscussion.value?.messages || []; expandedMsgs.value = new Set(ms.length ? [ms[ms.length - 1].id] : []); refitFrames() } -watch(coordToken, () => { svcStatus.value = null; expandLast(); replyOpen.value = false; summaryText.value = ''; summaryErr.value = ''; fetchAgents().then(a => { agents.value = a }) }) +watch(coordToken, () => { svcStatus.value = null; expandLast(); replyOpen.value = false; summaryText.value = ''; summaryErr.value = ''; closedDrafts.value = new Set(); fetchAgents().then(a => { agents.value = a }) }) watch(() => (activeDiscussion.value?.messages || []).length, (n, o) => { if (n > (o || 0)) { const ms = activeDiscussion.value?.messages || []; if (ms.length) { const s = new Set(expandedMsgs.value); s.add(ms[ms.length - 1].id); expandedMsgs.value = s } } }) const gmailReplyUrl = computed(() => activeConv.value?.gmailReplyUrl || activeDiscussion.value?.gmailReplyUrl || null) const coordNotes = computed(() => activeConv.value?.notes || [])