2026 03 21 Whistle Group Chat Rewiring
Documentation for Plans
Whistle & Group Chat Rewiring — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Complete the wiring between existing whistle/group frontend components and backend services — 6 specific gaps identified by code audit.
Architecture: All components exist. WhistleButton, WhistleNotification, WhistleService, ParticipantStrip, ChatEngine, ChatShell — all built in S218. The issue is broken event flows and missing data fields. This plan reconnects them.
Tech Stack: Angular 20+, standalone components, signals, Socket.IO, existing NestJS endpoints.
Spec: docs/superpowers/specs/2026-03-14-whistle-group-chat-design.md (S218 original)
Audit source: S250 code audit (this session)
Git convention: No commits — concurrent session constraint.
Gap Map (from audit)
GAP 1: WhistleButton → ChatShell (event not handled)
GAP 2: Group session creation flow (missing entirely)
GAP 3: VittoChatSession + VittoSessionSummary missing group fields
GAP 4: UnifiedMessageComponent not group-aware (no sender names)
GAP 5: Socket.IO missing participant-joined listener
GAP 6: SessionSidebar not group-aware
Task 1: Wire WhistleButton → ChatShell → Backend
Gap: WhistleButton emits memberSelected → ChatHeader passes it up as whistleMemberSelected → ChatShell never handles it → WhistleService.whistle() never called.
Files:
Modify:
libs/libs-ai/vitto-chat/src/lib/components/chat-shell/chat-shell.component.htmlModify:
libs/libs-ai/vitto-chat/src/lib/components/chat-shell/chat-shell.component.tsStep 1: Read the chat-shell template to find where ChatHeader is rendered
Read libs/libs-ai/vitto-chat/src/lib/components/chat-shell/chat-shell.component.html and find the <lib-chat-header> tag. Note the exact line and existing bindings.
- Step 2: Add event binding to chat-shell template
On the <lib-chat-header> tag, add:
(whistleMemberSelected)="onWhistleMemberSelected($event)"
- Step 3: Add handler method to chat-shell component
In chat-shell.component.ts, add:
async onWhistleMemberSelected(member: { userId: string; displayName: string }) {
const session = this.chatEngine.activeSession();
if (!session) return;
await this.whistleService.whistle({
sessionId: session.sessionId,
targetUserId: member.userId,
targetDisplayName: member.displayName,
whistlerUserId: this.chatEngine.userId(),
whistlerDisplayName: this.chatEngine.userName(),
historyAccess: 'summary',
urgency: 'smart',
});
}
Verify WhistleService is injected in the constructor. If not, add: private readonly whistleService = inject(WhistleService);
- Step 4: Verify the WhistleService.whistle() method POSTs correctly
Read whistle.service.ts and confirm it POSTs to /vitto-api/engine/whistle with the right payload. If the method signature doesn't match what we're calling, adapt the call.
Task 2: Add Group Fields to Frontend Session Models
Gap: VittoChatSession and VittoSessionSummary have no isGroup, participants, or groupConversationId fields. The sidebar, message renderer, and engine can't distinguish group from 1-on-1.
Files:
Modify:
libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-persistence.service.ts(or wherever VittoChatSession is defined)Modify: session summary interface (wherever VittoSessionSummary is defined)
Step 1: Find and read the VittoChatSession interface
Search for interface VittoChatSession or VittoChatSession in libs/libs-ai/vitto-chat/src/. Read the full interface.
- Step 2: Add group fields
Add to the VittoChatSession interface:
isGroup?: boolean;
participants?: Array<{
userId: string;
displayName: string;
role: 'originator' | 'member';
joinedAt: number;
channel?: string;
}>;
groupConversationId?: string;
- Step 3: Find and update VittoSessionSummary
Search for interface VittoSessionSummary or similar. Add:
isGroup?: boolean;
participantCount?: number;
participantNames?: string[]; // For sidebar preview
- Step 4: Update session loading to include group fields
Find where sessions are fetched from the backend (GET /sessions/:id or GET /sessions/my). Ensure the response mapping includes the new fields. The backend already returns isGroup and participants — we just need to not strip them.
Task 3: Wire UnifiedMessageComponent for Group Sender Attribution
Gap: message-bubble.component.ts has senderInfo logic for groups, but UnifiedMessageComponent (the actual renderer in chat-shell) doesn't pass isGroup through. Group messages show "Vitto" instead of actual sender names.
Files:
Modify:
libs/libs-ai/vitto-chat/src/lib/components/chat-shell/unified-message.component.ts(or equivalent)Modify: corresponding
.htmltemplateStep 1: Read UnifiedMessageComponent
Find and read the component. Identify how it renders messages and where it decides the display name and avatar.
- Step 2: Add isGroup input
Add to the component:
isGroup = input(false);
- Step 3: Update display name logic
Where the component determines the display name (currently "You" vs "Vitto"), add group logic:
readonly displayName = computed(() => {
const msg = this.message();
if (msg.role === 'user') {
if (this.isGroup() && msg.senderId && msg.senderId !== this.currentUserId()) {
return msg.senderName || 'Unknown';
}
return 'You';
}
return 'Vitto';
});
- Step 4: Pass isGroup from chat-shell
In the chat-shell template where messages are rendered (likely a @for loop over messages), pass:
[isGroup]="isGroupSession()"
Task 4: Add Socket.IO Listener for Participant Joined
Gap: When someone accepts a whistle and joins a group session, the existing participants don't see the update until page refresh. Backend emits chat:participant-joined but frontend doesn't listen.
Files:
Modify:
libs/libs-ai/vitto-chat/src/lib/services/chat-realtime.service.ts(or wherever Socket.IO listeners are)Modify:
libs/libs-ai/vitto-chat/src/lib/services/vitto-chat-engine.service.tsStep 1: Read ChatRealtimeService
Find the Socket.IO service. Read how existing events (onMessagesReceived, onSessionUpdated) are wired.
- Step 2: Add participant-joined listener
Add a new observable/callback for the chat:participant-joined event:
onParticipantJoined(callback: (data: { sessionId: string; userId: string; displayName: string; joinedAt: number }) => void) {
this.socket.on('chat:participant-joined', callback);
}
- Step 3: Wire in engine service
In the engine service, where Socket.IO listeners are set up, add:
this.realtimeService.onParticipantJoined((data) => {
const session = this.activeSession();
if (session?.sessionId === data.sessionId) {
// Update participants list reactively
const current = session.participants || [];
if (!current.find(p => p.userId === data.userId)) {
this._activeSession.update(s => s ? {
...s,
participants: [...(s.participants || []), {
userId: data.userId,
displayName: data.displayName,
role: 'member' as const,
joinedAt: data.joinedAt,
}],
isGroup: true,
} : s);
}
}
});
Task 5: Update Session Sidebar for Group Awareness
Gap: Session list shows all sessions identically. No visual indicator for group sessions, no participant preview.
Files:
Modify:
libs/libs-ai/vitto-chat/src/lib/components/chat-shell/unified-session-sidebar.component.tsModify: corresponding
.htmltemplateStep 1: Read the sidebar component
Find and read unified-session-sidebar.component.ts. Understand how sessions are listed and what data is available.
- Step 2: Add group indicators to session list items
In the template, where each session is rendered, add conditional group indicator:
@if (session.isGroup) {
<span class="group-badge" title="Group conversation">
<span class="participant-count">{{ session.participantCount || 2 }}</span>
</span>
}
- Step 3: Add minimal styles
.group-badge {
display: inline-flex;
align-items: center;
gap: 2px;
font-size: 11px;
opacity: 0.7;
}
Task 6: Verify Full Whistle Flow End-to-End
- Step 1: Verify build compiles
Run: npx nx build backend-vitto-api --configuration=development 2>&1 | tail -10
Run: build the affected frontend libs
- Step 2: Trace the full flow manually
- Chat header renders whistle button ✓ (existing)
- User clicks member →
memberSelectedemits ✓ (existing) - ChatHeader passes to shell →
whistleMemberSelectedemits (existing) - ChatShell handles it → calls
whistleService.whistle()(Task 1 — NEW) - Backend processes whistle → Socket.IO emits
chat:whistledto target (existing) - Target sees WhistleNotificationComponent → clicks Join (existing)
chatEngine.acceptWhistle()loads group session (existing)- Session shows as group in sidebar (Task 5 — NEW)
- Messages show sender names (Task 3 — NEW)
- New participant joins → Socket.IO updates participant list (Task 4 — NEW)
Implementation Notes
What we are NOT changing
- Backend whistle endpoints (working)
- WhistleOrchestratorService (working)
- GroupOrchestrationService (working — wired in Tasks 1-5 of the channel plan)
- WhistleButtonComponent (working — renders, emits events)
- WhistleNotificationComponent (working — renders, handles join/decline)
- ParticipantStripComponent (working — renders avatars)
- Socket.IO
chat:whistledemission (working) acceptWhistle()/dismissWhistle()in engine (working)
What we ARE changing (6 targeted fixes)
- ChatShell: bind to header's whistle event, call backend
- Session models: add isGroup + participants fields
- Message renderer: show sender names in groups
- Socket.IO: listen for participant-joined
- Sidebar: show group badge
- Verify everything compiles and flows end-to-end
Risks
- WhistleService.whistle() may have a different method signature than what Task 1 Step 3 assumes. Task 1 Step 4 verifies this before proceeding.
- Session loading may strip unknown fields — if the HTTP response mapping uses a whitelist pattern, new fields won't come through. Task 2 Step 4 checks this.
- message-bubble vs UnifiedMessage — two different renderers may be used in different contexts. Task 3 Step 1 identifies which is active in chat-shell before modifying.