From 935128e26bd9270b634038fa3e4c5ba808e41ff5 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 23 Mar 2026 15:16:09 +0200 Subject: [PATCH] fix(team): dedup duplicate SendMessage entries and show lead online during provisioning - Add content-based dedup in handleGetData merge: when two directed messages have identical from+to+text within a 5-second window, keep only the first. Fixes duplicate display caused by both CLI and our persistInboxMessage writing to the same inbox file. - Reorder checks in getMemberDotClass/getPresenceLabel: check leadActivity before isTeamProvisioning so the lead shows green dot when its process is already running during team setup. --- src/main/ipc/teams.ts | 19 +++++++++++++++++++ src/renderer/utils/memberHelpers.ts | 11 +++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/ipc/teams.ts b/src/main/ipc/teams.ts index 3d83415f..a0d0eedc 100644 --- a/src/main/ipc/teams.ts +++ b/src/main/ipc/teams.ts @@ -557,6 +557,12 @@ async function handleGetData( // messageIds inside the same session (e.g. lead-turn-* re-emits). const leadProcessTextFingerprints = new Set(); + // Content-based dedup for SendMessage captures: Claude Code CLI and our + // persistInboxMessage both write to inboxes/{member}.json, producing two entries + // with identical content but different messageIds. Track content fingerprints + // (from+to+text) with timestamps to collapse them within a 5-second window. + const contentSeen = new Map(); // fingerprint → timestamp ms + const merged: typeof data.messages = []; const seen = new Set(); for (const msg of [...data.messages, ...live]) { @@ -572,6 +578,19 @@ async function handleGetData( } leadProcessTextFingerprints.add(fp); } + + // Content dedup for directed messages (SendMessage captures): + // same from+to+text within 5 seconds = duplicate from CLI + our persist. + if (typeof msg.to === 'string' && msg.to.trim().length > 0) { + const contentFp = `${msg.from}\0${msg.to}\0${(msg.text ?? '').replace(/\s+/g, ' ').slice(0, 100)}`; + const msgMs = Date.parse(msg.timestamp); + const existingMs = contentSeen.get(contentFp); + if (existingMs !== undefined && Math.abs(msgMs - existingMs) <= 5000) { + continue; // duplicate within 5s window — skip + } + contentSeen.set(contentFp, msgMs); + } + const key = keyFor(msg); if (seen.has(key)) continue; seen.add(key); diff --git a/src/renderer/utils/memberHelpers.ts b/src/renderer/utils/memberHelpers.ts index f6927173..e7d5b419 100644 --- a/src/renderer/utils/memberHelpers.ts +++ b/src/renderer/utils/memberHelpers.ts @@ -42,13 +42,15 @@ export function getMemberDotClass( ): string { if (member.status === 'terminated') return STATUS_DOT_COLORS.terminated; if (member.removedAt) return STATUS_DOT_COLORS.terminated; - if (isTeamProvisioning) return STATUS_DOT_COLORS.unknown; - if (isTeamAlive === false) return STATUS_DOT_COLORS.terminated; + // Lead activity check BEFORE provisioning fallback — when the lead process + // is running (CLI logs present), show green even during provisioning. if (leadActivity && isLeadMember(member)) { return leadActivity === 'active' ? `${STATUS_DOT_COLORS.active} animate-pulse` : STATUS_DOT_COLORS.active; } + if (isTeamProvisioning) return STATUS_DOT_COLORS.unknown; + if (isTeamAlive === false) return STATUS_DOT_COLORS.terminated; // When team is alive, all non-terminated members are online if (isTeamAlive) { if (member.currentTaskId) return `${STATUS_DOT_COLORS.active} animate-pulse`; @@ -67,8 +69,7 @@ export function getPresenceLabel( leadContextPercent?: number ): string { if (member.status === 'terminated') return 'terminated'; - if (isTeamProvisioning) return 'connecting'; - if (isTeamAlive === false) return 'offline'; + // Lead activity check before provisioning fallback (mirrors getMemberDotClass order). if (leadActivity && isLeadMember(member)) { if (leadActivity === 'active') { return leadContextPercent != null && leadContextPercent > 0 @@ -77,6 +78,8 @@ export function getPresenceLabel( } return 'ready'; } + if (isTeamProvisioning) return 'connecting'; + if (isTeamAlive === false) return 'offline'; if (member.status === 'unknown') return 'idle'; return member.currentTaskId ? 'working' : 'idle'; }