eleven / messenger Public

web: openThread never leaves a stale pane — load-on-miss + clear fallback (#674)

Opening a thread that wasn't loaded yet (programmatic open during onboarding
or right after a Discover/auto join) fell through renderAppFor→chatAppFor's
not-found path and mounted nothing, leaving the OUTGOING thread's feed under
the new header (Eleven's onboarding messages 'in' the just-joined open group;
re-navigating fixed it). Now: fetch-and-retry once when the thread is missing,
and clear the pane if there's still no feed to mount so a stale feed can never
sit under a new header.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 48d3c456660f0ef75346a619e94ce4d89bdd0029 parent b332a94
2 files changed, +25 −1
  • changelog/2026-07-11-stale-feed-fix.md +5 −0
  • web/static/shell.js +20 −1
diff --git a/changelog/2026-07-11-stale-feed-fix.md b/changelog/2026-07-11-stale-feed-fix.md
new file mode 100644
index 0000000..6680b32
--- /dev/null
+++ b/changelog/2026-07-11-stale-feed-fix.md
@@ -0,0 +1,5 @@
+🤖 **Fixed: a chat could briefly show another chat's messages.** Opening a
+group you'd just joined (or one you landed in during onboarding) could leave
+the previous chat's messages showing under the new chat's name until you
+tapped away and back. Chats now always load their own messages the first
+time you open them.
diff --git a/web/static/shell.js b/web/static/shell.js
index 16f27a5..4a6d1e0 100644
--- a/web/static/shell.js
+++ b/web/static/shell.js
@@ -2855,11 +2855,30 @@ async function openThread(id) {
// A personal app-thread (chat or interface view) or a chat-app thread renders
// full-pane (no original feed / composer). An app-thread defaults to its chat
// view — a DM with its bot — with the header toggle switching to the interface view.
- const renderApp = renderAppFor(id);
+ // Mount the thread's surface. renderAppFor → chatAppFor looks the thread up
+ // in `threads`, so a thread that isn't loaded yet (opened programmatically
+ // during onboarding, or right after a Discover/auto join) resolves to
+ // "no feed". Fetch once and retry rather than falling through — otherwise
+ // the block below would leave the PREVIOUS thread's pane showing.
+ let renderApp = renderAppFor(id);
+ if (!renderApp && !threads.some((t) => t.id === id)) {
+ await loadThreads();
+ if (myOpen !== openSeq) return; // a newer open superseded us
+ renderThreadList();
+ renderApp = renderAppFor(id);
+ }
if (renderApp) {
await openAppThreadView(id, myOpen, renderApp, threadApp[id] || null);
return;
}
+ // No feed to mount for this thread. Never leave the outgoing thread's feed
+ // sitting under the new header (the onboarding-then-join bug: Eleven's
+ // messages showing "in" the just-opened group) — clear the pane. stashAppPane
+ // above only detaches the old root when it was cached, so an uncached pane
+ // (e.g. the onboarding conversation) would otherwise linger here.
+ const pane = document.getElementById("app-pane");
+ if (pane) pane.textContent = "";
+ setAppThreadMode(false);
}
// Pin the feed to the bottom, and keep it pinned for a short settle window as