mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
fix: prevent repeat chat message rendering on reconnect/replay
Root cause: server persists chat to disk (chat.jsonl) and replays on restart. Client had no dedup, so every reconnect re-rendered the entire history. Messages from an old HN session would repeat endlessly on the SF Chronicle tab. Fix: renderedEntryIds Set tracks which entry IDs have been rendered. addChatEntry skips entries already in the set. Entries without an id (local notifications) bypass the check. Clear chat resets the set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)', () => {
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
Reference in New Issue
Block a user