From c006fad97db4d9bbe76c41651610ffa2e8d4cf3c Mon Sep 17 00:00:00 2001 From: iliya Date: Thu, 5 Mar 2026 19:12:41 +0200 Subject: [PATCH] feat: implement CLI auto-suffix name handling across team services - Added functionality to drop CLI auto-suffixed duplicates (e.g., "alice-2") when the base name exists in TeamConfigReader, TeamMemberResolver, and TeamMembersMetaStore. - Introduced createCliAutoSuffixNameGuard utility to manage name validation. - Updated ActivityItem component to handle system messages with distinct styling. - Enhanced CSS variables for system activity messages in index.css. - Refactored SortableTab and TeamTabSectionNav components for improved layout and accessibility. --- src/main/services/team/TeamConfigReader.ts | 10 ++++ src/main/services/team/TeamMemberResolver.ts | 10 ++++ .../services/team/TeamMembersMetaStore.ts | 20 +++++++ .../components/layout/SortableTab.tsx | 56 +++++++++---------- src/renderer/components/layout/TabBar.tsx | 2 +- .../components/layout/TeamTabSectionNav.tsx | 4 +- .../components/team/activity/ActivityItem.tsx | 19 +++++-- src/renderer/index.css | 10 ++++ src/shared/utils/teamMemberName.ts | 41 ++++++++++++++ 9 files changed, 132 insertions(+), 40 deletions(-) create mode 100644 src/shared/utils/teamMemberName.ts diff --git a/src/main/services/team/TeamConfigReader.ts b/src/main/services/team/TeamConfigReader.ts index 3f6a830c..d1de9a24 100644 --- a/src/main/services/team/TeamConfigReader.ts +++ b/src/main/services/team/TeamConfigReader.ts @@ -8,6 +8,7 @@ import { getTeamFsWorkerClient } from './TeamFsWorkerClient'; import { TeamMembersMetaStore } from './TeamMembersMetaStore'; import type { TeamConfig, TeamMember, TeamSummary, TeamSummaryMember } from '@shared/types'; +import { createCliAutoSuffixNameGuard } from '@shared/utils/teamMemberName'; const logger = createLogger('Service:TeamConfigReader'); @@ -244,6 +245,15 @@ export class TeamConfigReader { } } + // Defense: drop CLI auto-suffixed duplicates (alice-2) when base name exists. + const allNames = Array.from(memberMap.values()).map((m) => m.name); + const keepName = createCliAutoSuffixNameGuard(allNames); + for (const [key, member] of Array.from(memberMap.entries())) { + if (!keepName(member.name)) { + memberMap.delete(key); + } + } + const members = Array.from(memberMap.values()); const summary: TeamSummary = { teamName, diff --git a/src/main/services/team/TeamMemberResolver.ts b/src/main/services/team/TeamMemberResolver.ts index 313f3349..d2e5a42d 100644 --- a/src/main/services/team/TeamMemberResolver.ts +++ b/src/main/services/team/TeamMemberResolver.ts @@ -6,6 +6,8 @@ import type { TeamTaskWithKanban, } from '@shared/types'; +import { createCliAutoSuffixNameGuard } from '@shared/utils/teamMemberName'; + export class TeamMemberResolver { resolveMembers( config: TeamConfig, @@ -78,6 +80,14 @@ export class TeamMemberResolver { // (recipient of SendMessage to "user"). It's not a real AI teammate. names.delete('user'); + // Defense: hide CLI auto-suffixed duplicates (alice-2) when base name (alice) exists. + const keepName = createCliAutoSuffixNameGuard(names); + for (const name of Array.from(names)) { + if (!keepName(name)) { + names.delete(name); + } + } + const members: ResolvedTeamMember[] = []; for (const name of names) { const ownedTasks = tasks.filter((task) => task.owner === name); diff --git a/src/main/services/team/TeamMembersMetaStore.ts b/src/main/services/team/TeamMembersMetaStore.ts index 66ff8f43..1fb221ff 100644 --- a/src/main/services/team/TeamMembersMetaStore.ts +++ b/src/main/services/team/TeamMembersMetaStore.ts @@ -7,6 +7,8 @@ import { atomicWriteAsync } from './atomicWrite'; import type { TeamMember } from '@shared/types'; +import { createCliAutoSuffixNameGuard } from '@shared/utils/teamMemberName'; + interface TeamMembersMetaFile { version: 1; members: TeamMember[]; @@ -90,6 +92,15 @@ export class TeamMembersMetaStore { deduped.set(normalized.name, normalized); } + // Defense: drop CLI auto-suffixed duplicates (alice-2) when base name exists. + const allNames = Array.from(deduped.keys()); + const keepName = createCliAutoSuffixNameGuard(allNames); + for (const name of allNames) { + if (!keepName(name)) { + deduped.delete(name); + } + } + return Array.from(deduped.values()).sort((a, b) => a.name.localeCompare(b.name)); } @@ -103,6 +114,15 @@ export class TeamMembersMetaStore { deduped.set(normalized.name, normalized); } + // Defense: drop CLI auto-suffixed duplicates (alice-2) when base name exists. + const allNames = Array.from(deduped.keys()); + const keepName = createCliAutoSuffixNameGuard(allNames); + for (const name of allNames) { + if (!keepName(name)) { + deduped.delete(name); + } + } + const payload: TeamMembersMetaFile = { version: 1, members: Array.from(deduped.values()).sort((a, b) => a.name.localeCompare(b.name)), diff --git a/src/renderer/components/layout/SortableTab.tsx b/src/renderer/components/layout/SortableTab.tsx index 84dd9f36..bf366be6 100644 --- a/src/renderer/components/layout/SortableTab.tsx +++ b/src/renderer/components/layout/SortableTab.tsx @@ -125,11 +125,7 @@ export const SortableTab = ({ role="tab" tabIndex={0} aria-selected={isActive} - className={ - isTeamTab - ? 'group flex min-w-0 max-w-[200px] shrink-0 cursor-grab flex-col rounded-md' - : 'group flex min-w-0 max-w-[200px] shrink-0 cursor-grab items-center gap-2 rounded-md px-3 py-1.5' - } + className="group flex min-w-0 max-w-[200px] shrink-0 cursor-grab items-center gap-2 rounded-md px-3 py-1.5" style={style} onClick={(e) => onTabClick(tab.id, e)} onMouseDown={(e) => onMouseDown(tab.id, e)} @@ -143,32 +139,18 @@ export const SortableTab = ({ } }} > -
- - {tab.fromSearch && ( - - - - )} - {isPinned && ( - - - - )} - {tab.label} - -
+ + {tab.fromSearch && ( + + + + )} + {isPinned && ( + + + + )} + {tab.label} {isTeamTab && ( )} + ); }; diff --git a/src/renderer/components/layout/TabBar.tsx b/src/renderer/components/layout/TabBar.tsx index 4efd68ef..74b5f215 100644 --- a/src/renderer/components/layout/TabBar.tsx +++ b/src/renderer/components/layout/TabBar.tsx @@ -309,7 +309,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => { outline: isDroppableOver ? '1px dashed var(--color-accent, #6366f1)' : 'none', outlineOffset: '-1px', overflowX: 'auto', - overflowY: 'clip', + overflowY: 'hidden', } as React.CSSProperties } > diff --git a/src/renderer/components/layout/TeamTabSectionNav.tsx b/src/renderer/components/layout/TeamTabSectionNav.tsx index 913fe1ce..c43125ff 100644 --- a/src/renderer/components/layout/TeamTabSectionNav.tsx +++ b/src/renderer/components/layout/TeamTabSectionNav.tsx @@ -71,11 +71,11 @@ export const TeamTabSectionNav = ({ }, [open]); return ( -
e.stopPropagation()}> +
e.stopPropagation()}>