Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 04aca127f687555898a105a2c443ff35b6a3c670)
+++ src/services/ActivityPubService.js	(revision 7875a80e55bfc41b72ae6716ca804d1893175379)
@@ -639,8 +639,23 @@
       items.push({
         type: 'sent', outboxId: m.id, to_handle: m.to_handle, in_reply_to: m.in_reply_to,
-        content: m.content, editable: m.editable, language: m.language, created_at: m.created_at,
+        post_slug: m.post_slug, content: m.content, editable: m.editable,
+        language: m.language, created_at: m.created_at,
       });
     }
   } catch { /* ignore */ }
+  // Een verzonden antwoord kent zijn post_slug maar niet de titel (ap_outbox
+  // bewaart die niet). Zonder titel toont een draad waarin JIJ als enige iets
+  // zei alleen een slug, dus vullen we ze in één query aan.
+  try {
+    const missing = [...new Set(items.filter((i) => i.post_slug && !i.post_title).map((i) => i.post_slug))];
+    if (missing.length) {
+      const rows = db.prepare(
+        `SELECT slug, title FROM posts WHERE slug IN (${missing.map(() => '?').join(',')})
+           AND site_id = (SELECT id FROM sites WHERE slug = ?)`,
+      ).all(...missing, slug);
+      const byslug = new Map(rows.map((r) => [r.slug, r.title]));
+      for (const i of items) if (i.post_slug && !i.post_title) i.post_title = byslug.get(i.post_slug) || null;
+    }
+  } catch { /* zonder titel valt de draad terug op de slug */ }
   items.sort((a, b) => _msgTs(b) - _msgTs(a)); // NaN-safe (zie getNotifications)
   const out = [];
@@ -656,5 +671,89 @@
     out.push(it);
   }
-  return out.slice(off, off + lim);
+  // Antwoorden, mentions en je eigen verzonden berichten vouwen samen tot
+  // draden; likes/boosts/follows/reports blijven losse regels. Na deze stap
+  // telt een draad als één item voor de paginering, wat klopt: je scrolt door
+  // gesprekken, niet door losse zinnen.
+  return groupConversations(out).slice(off, off + lim);
+}
+
+// De drie soorten die samen een gesprek vormen. Vroeger zaten ze in drie
+// aparte chips: 'reply' en 'mention' onder Berichten/Gesprekken (afhankelijk van
+// de zichtbaarheid) en 'sent' onder Verzonden. Wie een uitwisseling wilde volgen
+// moest dus tussen chips heen en weer, terwijl het één draad is.
+const CONV_TYPES = new Set(['reply', 'mention', 'sent']);
+
+/** Waar hangt dit bericht aan? Twee soorten draden, en de volgorde telt:
+ *
+ *  1. Aan een post van jou. Een ontvangen antwoord kent zijn post via de join
+ *     op `posts`, een verzonden antwoord via ap_outbox.post_slug. Dat is
+ *     dezelfde sleutel, en daarom staan ze nu in dezelfde draad.
+ *  2. Aan een persoon. Een mention hangt aan niets van jou (het is iemands
+ *     eigen post waarin je genoemd wordt) en heeft geen post_slug; die draad
+ *     loopt per tegenpartij.
+ *
+ *  De post wint van de persoon: twee mensen die onder dezelfde post reageren
+ *  voeren één gesprek, geen twee. Geeft null terug voor alles wat geen gesprek
+ *  is (likes, boosts, follows, reports, poll-uitslagen); die stromen ongemoeid
+ *  door.
+ */
+export function threadKey(it) {
+  if (!it || !CONV_TYPES.has(it.type)) return null;
+  if (it.post_slug) return `post:${it.post_slug}`;
+  const who = it.handle || it.to_handle || '';
+  const norm = String(who).trim().toLowerCase().replace(/^@/, '');
+  return norm ? `actor:${norm}` : null;
+}
+
+/** Vouw losse berichten samen tot draden, met alles wat geen gesprek is
+ *  ongemoeid ertussen. Verwacht [items] al gesorteerd op created_at aflopend
+ *  (zoals getMessages ze aanlevert); de draad komt daardoor op de plek van zijn
+ *  nieuwste bericht te staan en `created_at` van de draad IS dat bericht. Binnen
+ *  de draad draait het om: een gesprek leest naar beneden, oud naar nieuw.
+ */
+export function groupConversations(items) {
+  const threads = new Map();
+  const out = [];
+  for (const it of items || []) {
+    const key = threadKey(it);
+    if (!key) { out.push(it); continue; }
+    let t = threads.get(key);
+    if (!t) {
+      // Eerste keer dat we deze draad zien = het nieuwste bericht erin, want de
+      // invoer is aflopend gesorteerd. Vandaar created_at hier en niet later.
+      t = { type: 'thread', key, post: null, people: [], messages: [], created_at: it.created_at };
+      threads.set(key, t);
+      out.push(t);
+    }
+    t.messages.push(it);
+    // De context bij de draad: gaat het over een post, dan hoort de link
+    // erbij, anders is een los antwoord in een lijst niet te plaatsen.
+    // De titel blijft LEEG zolang hij onbekend is, in plaats van terug te
+    // vallen op de slug: het nieuwste bericht in een draad is vaak je eigen
+    // verzonden antwoord, en dat kent alleen de slug. Zou die de titel worden,
+    // dan kan het ontvangen antwoord eronder de echte titel niet meer
+    // invullen. De terugval op de slug hoort in de weergave, niet in de data.
+    if (it.post_slug) {
+      if (!t.post) t.post = { slug: it.post_slug, title: it.post_title || null };
+      else if (!t.post.title && it.post_title) t.post.title = it.post_title;
+    }
+  }
+  for (const t of threads.values()) {
+    t.messages.sort((a, b) => _msgTs(a) - _msgTs(b));
+    t.count = t.messages.length;
+    // Wie zit er in dit gesprek, jij niet meegerekend: 'sent' ben jij.
+    const seen = new Set();
+    for (const m of t.messages) {
+      if (m.type === 'sent') continue;
+      const h = m.handle || m.name;
+      if (!h || seen.has(h)) continue;
+      seen.add(h);
+      t.people.push({ name: m.name, handle: m.handle, icon: m.icon, url: m.url });
+    }
+    // Heb JIJ in deze draad iets gezegd? Bepaalt of hij als uitwisseling of als
+    // onbeantwoord bericht leest.
+    t.mine = t.messages.some((m) => m.type === 'sent');
+  }
+  return out;
 }
 
@@ -2907,5 +3006,9 @@
 }
 export function listOutbox(siteSlug) {
-  return db.prepare('SELECT id, content, to_handle, in_reply_to, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
+  // post_slug reist mee sinds Berichten gesprekken toont: het is de sleutel
+  // waarop een verzonden antwoord bij de ontvangen antwoorden op dezelfde post
+  // gaat staan (zie threadKey). Zonder die kolom viel een uitwisseling uit
+  // elkaar in "Verzonden" en "Gesprekken".
+  return db.prepare('SELECT id, content, to_handle, to_actor, post_slug, in_reply_to, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
     .all(siteSlug).map((r) => { const c = stripLeadingMentions(r.content); return { ...r, content: c, editable: outboxEditableText(c) }; });
 }
Index: src/views/pages/messages.ejs
===================================================================
--- src/views/pages/messages.ejs	(revision 04aca127f687555898a105a2c443ff35b6a3c670)
+++ src/views/pages/messages.ejs	(revision 7875a80e55bfc41b72ae6716ca804d1893175379)
@@ -25,10 +25,11 @@
 
   <div class="msg-filters" role="tablist" aria-label="<%= t('msg.title') %>">
+    <%# Vier chips, geen zes: Berichten, Gesprekken en Verzonden gingen op in
+        een enkele Gesprekken-view, waarin verzonden en ontvangen in dezelfde
+        draad staan. Activiteit en Moderatie blijven wat ze waren. %>
     <button type="button" class="msg-chip is-on" data-show="all"><%= t('msg.filter_all') %></button>
-    <button type="button" class="msg-chip" data-show="msgs"><%= t('msg.filter_msgs') %></button>
     <button type="button" class="msg-chip" data-show="conv"><%= t('msg.filter_conv') %></button>
     <button type="button" class="msg-chip" data-show="act"><%= t('msg.filter_act') %></button>
     <button type="button" class="msg-chip" data-show="mod"><%= t('msg.filter_mod') %></button>
-    <button type="button" class="msg-chip" data-show="sent"><%= t('msg.filter_sent') %></button>
   </div>
   <div class="msg-search">
@@ -71,4 +72,10 @@
   var items = [];
   function indexItem(li) {
+    // Een draad indexeren op zijn eerste bubbel zou de rest onvindbaar maken,
+    // dus daar nemen we de hele tekst; losse regels blijven gericht geindexeerd.
+    if (li.classList.contains('msg-thread')) {
+      li._search = ((li.getAttribute('data-who') || '') + ' ' + li.textContent).toLowerCase();
+      return;
+    }
     // Index once: sender + message body + linked post title + poll text.
     var body = li.querySelector('.msg-content');
@@ -82,5 +89,5 @@
   // items join the filter/search (and inherit the active chip via apply()).
   function reindex() {
-    items = list ? Array.prototype.slice.call(list.querySelectorAll('.msg-item')) : [];
+    items = list ? Array.prototype.slice.call(list.querySelectorAll(':scope > .msg-item')) : [];
     items.forEach(function (li) { if (!li._search) indexItem(li); });
   }
@@ -138,4 +145,24 @@
 
   .msg-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
+
+  /* Een draad is geen regel maar een blok: kop met de context, daaronder de
+     bubbels van oud naar nieuw. De verticale lijn links maakt zichtbaar dat ze
+     bij elkaar horen -- dat was precies wat ontbrak toen verzonden en ontvangen
+     in aparte chips zaten. */
+  .msg-thread { display: block; }
+  .msg-thread-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
+  .msg-thread-who { font-weight: 600; }
+  .msg-thread-count { font-size: .78em; padding: .05rem .4rem; border-radius: 999px;
+    background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
+  .msg-thread-post { display: inline-flex; align-items: center; gap: .3rem; font-size: .85em;
+    margin-inline-start: auto; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+  .msg-thread-post svg { width: 13px; height: 13px; flex: none; }
+  .msg-thread-msgs { list-style: none; margin: 0; padding: 0 0 0 .85rem; display: flex; flex-direction: column; gap: .45rem;
+    border-inline-start: 2px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); }
+  .msg-sub { display: flex; gap: .8rem; align-items: flex-start; }
+  /* Jouw eigen bijdrage staat aan de andere kant van de lijn, zodat je de
+     beurten ziet zonder namen te lezen. */
+  .msg-sub.msg-sent { flex-direction: row-reverse; text-align: end; }
+  .msg-sub.msg-sent .msg-line { justify-content: flex-end; }
 
   .msg-item { display: flex; gap: .8rem; padding: .85rem .95rem; border-radius: 14px; align-items: flex-start;
Index: src/views/partials/msg-item.ejs
===================================================================
--- src/views/partials/msg-item.ejs	(revision 04aca127f687555898a105a2c443ff35b6a3c670)
+++ src/views/partials/msg-item.ejs	(revision 7875a80e55bfc41b72ae6716ca804d1893175379)
@@ -1,2 +1,34 @@
+<% if (n.type === 'thread') { %>
+<%
+           var _tseen = (typeof seen !== 'undefined' && seen) ? seen : 0;
+           // Een draad is nieuw zodra EEN bericht erin nieuw is.
+           var _tnew = !!_tseen && (n.messages || []).some(function (m) {
+             return m.created_at && Date.parse(m.created_at) > _tseen;
+           });
+           var _tpeople = (n.people || []).map(function (p) { return p.name || p.handle || '?'; });
+           // Alleen jij aan het woord: dan is de tegenpartij het adres waaraan
+           // je schreef, niet een lege naam.
+           var _tto = ((n.messages || []).find(function (m) { return m.to_handle; }) || {}).to_handle;
+           var _tlabel = _tpeople.length ? _tpeople.join(', ') : (_tto || t('msg.you'));
+      %>
+        <li class="msg-item msg-thread<%= _tnew ? ' is-new' : '' %>" data-kind="conv" data-who="<%= (_tpeople.join(' ') + ' ' + (_tto || '')).toLowerCase() %>">
+          <div class="msg-thread-head">
+            <span class="msg-thread-who"><%= _tlabel %></span>
+            <% if (n.count > 1) { %><span class="msg-thread-count"><%= n.count %></span><% } %>
+            <% if (_tnew) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
+            <%# De context bij de draad: waar gaat dit gesprek over? Zonder deze
+                link is een antwoord in een lijst niet te plaatsen. %>
+            <% if (n.post) { %>
+              <a class="msg-thread-post" href="/<%= n.post.slug %>">
+                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
+                <%= n.post.title || n.post.slug %>
+              </a>
+            <% } %>
+          </div>
+          <ul class="msg-thread-msgs">
+            <% (n.messages || []).forEach(function (m) { %><%- include('msg-item', { n: m, seen: _tseen, inThread: true }) %><% }); %>
+          </ul>
+        </li>
+<% } else { %>
 <%
            var _seen = (typeof seen !== 'undefined' && seen) ? seen : 0;
@@ -13,8 +45,11 @@
              : (_t === 'mention' || _priv0) ? 'msgs'
              : 'conv';
+           // Binnen een draad is dit geen zelfstandige regel: de chip filtert op
+           // de draad, niet op de losse zinnen erin.
+           var _sub = (typeof inThread !== 'undefined' && inThread);
            var _init = String(_who || '?').replace(/^@/, '').charAt(0).toUpperCase();
            var _priv = (n.visibility === 'followers' || n.visibility === 'direct');
       %>
-        <li class="msg-item msg-<%= _t %><%= _new ? ' is-new' : '' %>" data-kind="<%= _kind %>" data-who="<%= String(_who).toLowerCase() %>">
+        <li class="<%= _sub ? 'msg-sub' : 'msg-item' %> msg-<%= _t %><%= _new ? ' is-new' : '' %>"<% if (!_sub) { %> data-kind="<%= _kind %>"<% } %> data-who="<%= String(_who).toLowerCase() %>">
           <span class="msg-av<%= _t === 'sent' ? ' msg-av-sent' : '' %>" aria-hidden="true">
             <% if (_t === 'sent') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
@@ -65,5 +100,5 @@
               <% } else { %><%= t('notif.replied') %><% } %>
               <% if (_priv) { %><span class="msg-priv" title="<%= t('msg.private_hint') %>">🔒 <%= t('msg.private') %></span><% } %>
-              <% if (n.post_slug) { %><a class="msg-post" href="/<%= n.post_slug %>"><%= n.post_title || n.post_slug %></a><% } %>
+              <% if (n.post_slug && !_sub) { %><a class="msg-post" href="/<%= n.post_slug %>"><%= n.post_title || n.post_slug %></a><% } %>
               <% if (_t === 'mention' && n.note_url) { %><a class="msg-post" href="<%= n.note_url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
               <% if (_t === 'sent' && n.in_reply_to) { %><a class="msg-post" href="<%= n.in_reply_to %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
@@ -116,2 +151,3 @@
           </div>
         </li>
+<% } %>
Index: test/messages-conversations.test.js
===================================================================
--- test/messages-conversations.test.js	(revision 7875a80e55bfc41b72ae6716ca804d1893175379)
+++ test/messages-conversations.test.js	(revision 7875a80e55bfc41b72ae6716ca804d1893175379)
@@ -0,0 +1,113 @@
+// Berichten als gesprekken: antwoorden, mentions en je eigen verzonden
+// berichten vouwen samen tot draden, de rest van de stroom blijft ongemoeid.
+// Dit dekt de groepeerlogica zelf (zuiver, geen DB, geen netwerk); het
+// samenstellen van de stroom zit in getMessages en leunt op de database.
+//
+// Run: npm test
+
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
+
+const dbMod = await import('../src/config/database.js');
+dbMod.initializeDatabase();
+const AP = await import('../src/services/ActivityPubService.js');
+
+const { threadKey, groupConversations } = AP;
+
+// Aflopend gesorteerd, zoals getMessages ze aanlevert.
+const ts = (n) => new Date(Date.UTC(2026, 0, 1, 12, n)).toISOString();
+
+test('threadKey: een post wint van een persoon', () => {
+  assert.equal(threadKey({ type: 'reply', post_slug: 'hallo', handle: '@anna@a.test' }), 'post:hallo');
+  assert.equal(threadKey({ type: 'sent', post_slug: 'hallo', to_handle: '@anna@a.test' }), 'post:hallo');
+  // Zonder post loopt de draad per tegenpartij, hoofdletter- en @-ongevoelig.
+  assert.equal(threadKey({ type: 'mention', handle: '@Anna@A.test' }), 'actor:anna@a.test');
+  assert.equal(threadKey({ type: 'sent', to_handle: 'Anna@a.test' }), 'actor:anna@a.test');
+});
+
+test('threadKey: alles wat geen gesprek is krijgt geen draad', () => {
+  for (const type of ['like', 'announce', 'follow', 'report', 'poll_done']) {
+    assert.equal(threadKey({ type, post_slug: 'hallo' }), null, `${type} hoort geen draad te krijgen`);
+  }
+  assert.equal(threadKey(null), null);
+  assert.equal(threadKey({ type: 'mention' }), null, 'een mention zonder afzender heeft geen sleutel');
+});
+
+test('ontvangen en verzonden op dezelfde post komen in EEN draad', () => {
+  const out = groupConversations([
+    { type: 'sent', post_slug: 'hallo', content: 'graag gedaan', created_at: ts(3) },
+    { type: 'reply', post_slug: 'hallo', post_title: 'Hallo fediverse', handle: '@anna@a.test', name: 'Anna', content: 'dank!', created_at: ts(2) },
+    { type: 'reply', post_slug: 'hallo', post_title: 'Hallo fediverse', handle: '@bo@b.test', name: 'Bo', content: 'mooi', created_at: ts(1) },
+  ]);
+  assert.equal(out.length, 1, 'drie berichten, één draad');
+  const t = out[0];
+  assert.equal(t.type, 'thread');
+  assert.equal(t.count, 3);
+  // De context hoort bij de draad: dit gesprek gaat over een post.
+  assert.deepEqual(t.post, { slug: 'hallo', title: 'Hallo fediverse' });
+  // Een gesprek leest naar beneden: oud → nieuw.
+  assert.deepEqual(t.messages.map((m) => m.content), ['mooi', 'dank!', 'graag gedaan']);
+  // De draad staat op de tijd van zijn NIEUWSTE bericht, zodat een levend
+  // gesprek bovenaan komt.
+  assert.equal(t.created_at, ts(3));
+  // Jij zit er niet bij als deelnemer, de anderen wel.
+  assert.deepEqual(t.people.map((p) => p.name), ['Bo', 'Anna']);
+  assert.equal(t.mine, true);
+});
+
+test('een mention zonder post loopt per persoon, niet per post', () => {
+  const out = groupConversations([
+    { type: 'sent', to_handle: '@cas@c.test', content: 'hoi terug', created_at: ts(2) },
+    { type: 'mention', handle: '@cas@c.test', name: 'Cas', content: 'hoi!', created_at: ts(1) },
+  ]);
+  assert.equal(out.length, 1);
+  assert.equal(out[0].key, 'actor:cas@c.test');
+  assert.equal(out[0].post, null, 'zonder post is er geen context-link');
+  assert.deepEqual(out[0].messages.map((m) => m.content), ['hoi!', 'hoi terug']);
+});
+
+test('twee posts blijven twee draden, op volgorde van hun nieuwste bericht', () => {
+  const out = groupConversations([
+    { type: 'reply', post_slug: 'twee', post_title: 'Twee', handle: '@anna@a.test', created_at: ts(4) },
+    { type: 'reply', post_slug: 'een', post_title: 'Een', handle: '@bo@b.test', created_at: ts(3) },
+    { type: 'reply', post_slug: 'twee', post_title: 'Twee', handle: '@bo@b.test', created_at: ts(2) },
+  ]);
+  assert.deepEqual(out.map((t) => t.key), ['post:twee', 'post:een']);
+  assert.equal(out[0].count, 2);
+  assert.equal(out[1].count, 1);
+});
+
+test('likes, boosts en follows stromen ongemoeid tussen de draden door', () => {
+  const out = groupConversations([
+    { type: 'like', post_slug: 'hallo', handle: '@anna@a.test', created_at: ts(4) },
+    { type: 'reply', post_slug: 'hallo', post_title: 'Hallo', handle: '@bo@b.test', created_at: ts(3) },
+    { type: 'follow', handle: '@cas@c.test', created_at: ts(2) },
+    { type: 'report', handle: '@dee@d.test', created_at: ts(1) },
+  ]);
+  assert.deepEqual(out.map((i) => i.type), ['like', 'thread', 'follow', 'report']);
+  // Een like op dezelfde post trekt die post NIET de draad in: het is geen
+  // gesprek, en Activiteit blijft zijn eigen chip houden.
+  assert.equal(out[0].post_slug, 'hallo');
+});
+
+test('de titel komt van welk bericht in de draad hem ook maar kent', () => {
+  // Een verzonden antwoord kent alleen de slug; een ontvangen antwoord de titel.
+  const out = groupConversations([
+    { type: 'sent', post_slug: 'hallo', created_at: ts(2) },
+    { type: 'reply', post_slug: 'hallo', post_title: 'Hallo fediverse', handle: '@anna@a.test', created_at: ts(1) },
+  ]);
+  assert.deepEqual(out[0].post, { slug: 'hallo', title: 'Hallo fediverse' });
+});
+
+test('lege en rommelige invoer levert geen kapotte draden op', () => {
+  assert.deepEqual(groupConversations([]), []);
+  assert.deepEqual(groupConversations(null), []);
+  // Een gesprekssoort zonder sleutel blijft een losse regel in plaats van te
+  // verdwijnen: nooit een bericht kwijtraken in de groepering.
+  const out = groupConversations([{ type: 'sent', content: 'aan niemand', created_at: ts(1) }]);
+  assert.equal(out.length, 1);
+  assert.equal(out[0].type, 'sent');
+});
Index: test/messages.test.js
===================================================================
--- test/messages.test.js	(revision 04aca127f687555898a105a2c443ff35b6a3c670)
+++ test/messages.test.js	(revision 7875a80e55bfc41b72ae6716ca804d1893175379)
@@ -2,4 +2,8 @@
 // in één stroom, groepeert opeenvolgende likes/boosts op dezelfde post, en geeft
 // visibility door (voor de privé-badge). Besluit Robin+Bart 2026-07-16.
+//
+// Sinds Berichten gesprekken toont vouwen antwoorden, mentions en je eigen
+// verzonden berichten samen tot draden (zie messages-conversations.test.js voor
+// de groepeerlogica zelf); likes, boosts en follows blijven losse regels.
 //
 // Run: npm test   (= node --test)
@@ -42,10 +46,16 @@
 const msgs = AP.getMessages('me', 50);
 
-test('eigen outbound reply zit als "sent" in de stroom (nieuwste eerst)', () => {
-  const sent = msgs.find((m) => m.type === 'sent');
+test('eigen outbound reply en het ontvangen antwoord staan in EEN draad', () => {
+  const thread = msgs.find((m) => m.type === 'thread');
+  assert.ok(thread, 'draad ontbreekt');
+  assert.equal(msgs[0], thread, 'de draad met het nieuwste bericht hoort bovenaan');
+  const sent = thread.messages.find((m) => m.type === 'sent');
   assert.ok(sent, 'sent-item ontbreekt');
   assert.equal(sent.outboxId, 'out1');
   assert.equal(sent.to_handle, '@dana@r.test');
-  assert.equal(msgs[0].type, 'sent', 'nieuwste item hoort bovenaan');
+  // Hier gaat het om: Dana's antwoord en het jouwe zaten in aparte chips
+  // (Gesprekken en Verzonden) en staan nu op volgorde in dezelfde draad.
+  assert.deepEqual(thread.messages.map((m) => m.type), ['reply', 'sent']);
+  assert.equal(thread.post.slug, 'mijn-post');
 });
 
@@ -58,8 +68,11 @@
 
 test('privé-reply draagt visibility voor de badge en heeft post-context', () => {
-  const reply = msgs.find((m) => m.type === 'reply');
+  const thread = msgs.find((m) => m.type === 'thread');
+  const reply = thread.messages.find((m) => m.type === 'reply');
   assert.equal(reply.visibility, 'direct');
   assert.equal(reply.post_slug, 'mijn-post');
   assert.equal(reply.post_title, 'Mijn post');
+  // De context hangt ook aan de draad zelf: die voedt de link in de kop.
+  assert.deepEqual(thread.post, { slug: 'mijn-post', title: 'Mijn post' });
 });
 
