From 5e704ddc78a168fdc362a311a1c569a5ca655488 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 9 Mar 2026 20:53:45 +0200 Subject: [PATCH] feat: add cross-team messaging UI with team selector and visual distinction - Team selector dropdown in MessageComposer (left of recipient selector) - Cross-team mode locks recipient to team-lead, disables attachments - Sender copy written to fromTeam inbox with source 'cross_team_sent' - Purple accent styling for cross-team messages (incoming + outgoing) - "from team-x" / "to team-x" badges in ActivityItem - Strip [Cross-team from ...] prefix in UI (kept in stored text for CLI) - CSS variables for cross-team theme (dark + light) - Store: fetchCrossTeamTargets (all teams), sendCrossTeamMessage action - Tests updated for sender copy and formatted text prefix --- src/main/services/team/CrossTeamService.ts | 21 ++- src/main/services/team/TeamInboxWriter.ts | 2 +- .../components/team/TeamDetailView.tsx | 11 ++ .../components/team/activity/ActivityItem.tsx | 81 ++++++-- .../team/messages/MessageComposer.tsx | 174 ++++++++++++++++-- src/renderer/index.css | 10 + src/renderer/store/slices/teamSlice.ts | 40 ++++ src/shared/types/team.ts | 5 +- .../services/team/CrossTeamService.test.ts | 20 +- 9 files changed, 328 insertions(+), 36 deletions(-) diff --git a/src/main/services/team/CrossTeamService.ts b/src/main/services/team/CrossTeamService.ts index 327cefd6..6ddbcfaa 100644 --- a/src/main/services/team/CrossTeamService.ts +++ b/src/main/services/team/CrossTeamService.ts @@ -76,7 +76,7 @@ export class CrossTeamService { const formattedText = `[Cross-team from ${from} | depth:${chainDepth}]\n${text}`; const messageId = randomUUID(); - // 5. Inbox write (TeamInboxWriter handles file lock + in-process lock internally) + // 5. Inbox write to TARGET team (TeamInboxWriter handles file lock + in-process lock internally) await this.inboxWriter.sendMessage(toTeam, { member: leadName, text: formattedText, @@ -85,7 +85,24 @@ export class CrossTeamService { source: 'cross_team', }); - // 6. Best-effort relay (if online) + // 6. Write "sent" copy to SENDER's inbox so the message appears in their activity + const senderLeadName = (await this.dataService.getLeadMemberName(fromTeam)) ?? 'team-lead'; + void this.inboxWriter + .sendMessage(fromTeam, { + member: senderLeadName, + text, + from: 'user', + to: `${toTeam}.${leadName}`, + summary: summary ?? `Cross-team message to ${toTeam}`, + source: 'cross_team_sent', + }) + .catch((e: unknown) => { + logger.warn( + `Failed to write sender copy for ${fromTeam}: ${e instanceof Error ? e.message : String(e)}` + ); + }); + + // 7. Best-effort relay (if online) if (this.provisioning?.isTeamAlive(toTeam)) { void this.provisioning.relayLeadInboxMessages(toTeam).catch((e: unknown) => { logger.warn(`Cross-team relay to ${toTeam}: ${e instanceof Error ? e.message : String(e)}`); diff --git a/src/main/services/team/TeamInboxWriter.ts b/src/main/services/team/TeamInboxWriter.ts index e267b220..c8e55f07 100644 --- a/src/main/services/team/TeamInboxWriter.ts +++ b/src/main/services/team/TeamInboxWriter.ts @@ -23,7 +23,7 @@ export class TeamInboxWriter { const payload: InboxMessage = { from: request.from ?? 'user', - to: request.member, + to: request.to ?? request.member, text: request.text, timestamp: new Date().toISOString(), read: false, diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index a5f13521..905c35f6 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -215,6 +215,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele updateTaskStatus, updateTaskOwner, sendTeamMessage, + sendCrossTeamMessage, requestReview, createTeamTask, startTask, @@ -255,6 +256,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele updateTaskStatus: s.updateTaskStatus, updateTaskOwner: s.updateTaskOwner, sendTeamMessage: s.sendTeamMessage, + sendCrossTeamMessage: s.sendCrossTeamMessage, requestReview: s.requestReview, createTeamTask: s.createTeamTask, startTask: s.startTask, @@ -1567,6 +1569,15 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele }); }); }} + onCrossTeamSend={(toTeam, text, summary) => { + void sendCrossTeamMessage({ + fromTeam: teamName, + fromMember: 'user', + toTeam, + text, + summary, + }); + }} />
+ + +
+ {/* Current team option */} + + + {/* Separator */} +
+ + {/* Other teams */} + {crossTeamTargets.map((target) => { + const isSelected = selectedTeam === target.teamName; + return ( + + ); + })} +
+ + + ) : null} + +