diff --git a/browse/test/sidebar-ux.test.ts b/browse/test/sidebar-ux.test.ts index c03b18cd..a96f57a2 100644 --- a/browse/test/sidebar-ux.test.ts +++ b/browse/test/sidebar-ux.test.ts @@ -940,6 +940,40 @@ describe('chat toolbar buttons disabled state', () => { }); }); +// ─── Chat message dedup ───────────────────────────────────────── + +describe('chat message dedup (prevents repeat rendering)', () => { + const js = fs.readFileSync(path.join(ROOT, '..', 'extension', 'sidepanel.js'), 'utf-8'); + + test('renderedEntryIds Set exists for dedup tracking', () => { + expect(js).toContain('const renderedEntryIds = new Set()'); + }); + + test('addChatEntry checks entry.id against renderedEntryIds', () => { + const addFn = js.slice( + js.indexOf('function addChatEntry(entry)'), + js.indexOf('\n // User messages', js.indexOf('function addChatEntry(entry)')), + ); + expect(addFn).toContain('renderedEntryIds.has(entry.id)'); + expect(addFn).toContain('renderedEntryIds.add(entry.id)'); + // Should return early (skip) if already rendered + expect(addFn).toContain('return'); + }); + + test('addChatEntry skips dedup for entries without id (local notifications)', () => { + const addFn = js.slice( + js.indexOf('function addChatEntry(entry)'), + js.indexOf('\n // User messages', js.indexOf('function addChatEntry(entry)')), + ); + // Should only check dedup when entry.id is defined + expect(addFn).toContain('entry.id !== undefined'); + }); + + test('clear chat resets renderedEntryIds', () => { + expect(js).toContain('renderedEntryIds.clear()'); + }); +}); + // ─── LLM-based cleanup architecture ───────────────────────────── describe('LLM-based cleanup (smart agent cleanup)', () => { diff --git a/extension/sidepanel.js b/extension/sidepanel.js index bfc28e88..9e6626fc 100644 --- a/extension/sidepanel.js +++ b/extension/sidepanel.js @@ -102,7 +102,17 @@ let agentContainer = null; // The container for the current agent response let agentTextEl = null; // The text accumulator element let agentText = ''; // Accumulated text +// Dedup: track which entry IDs have already been rendered to prevent +// repeat rendering on reconnect or tab switch (server replays from disk) +const renderedEntryIds = new Set(); + function addChatEntry(entry) { + // Dedup by entry ID — prevent repeat rendering on reconnect/replay + if (entry.id !== undefined) { + if (renderedEntryIds.has(entry.id)) return; + renderedEntryIds.add(entry.id); + } + // Remove welcome message on first real message const welcome = chatMessages.querySelector('.chat-welcome'); if (welcome) welcome.remove(); @@ -577,6 +587,7 @@ document.getElementById('clear-chat').addEventListener('click', async () => { } catch {} // Reset local state chatLineCount = 0; + renderedEntryIds.clear(); agentContainer = null; agentTextEl = null; agentText = '';