diff --git a/src/renderer/components/team/members/MemberDetailHeader.tsx b/src/renderer/components/team/members/MemberDetailHeader.tsx index 7a0ffc8d..f6b910b4 100644 --- a/src/renderer/components/team/members/MemberDetailHeader.tsx +++ b/src/renderer/components/team/members/MemberDetailHeader.tsx @@ -82,8 +82,14 @@ export const MemberDetailHeader = ({ leadActivity, }); const presenceLabel = launchPresentation.presenceLabel; + const launchVisualState = launchPresentation.launchVisualState; + const launchStatusLabel = launchPresentation.launchStatusLabel; const dotClass = launchPresentation.dotClass; const runtimeAdvisoryTitle = launchPresentation.runtimeAdvisoryTitle; + const badgeLabel = + launchVisualState === 'runtime_pending' || launchVisualState === 'permission_pending' + ? (launchStatusLabel ?? presenceLabel) + : presenceLabel; const canEditRole = !isLeadMember(member) && !member.removedAt && !isTeamProvisioning && !!onUpdateRole; @@ -144,7 +150,7 @@ export const MemberDetailHeader = ({ className="px-1.5 py-0.5 text-[10px] font-normal leading-none text-[var(--color-text-muted)]" title={runtimeAdvisoryTitle} > - {presenceLabel} + {badgeLabel} {/* NOTE: lead context token display disabled — usage formula is inaccurate */} diff --git a/src/renderer/components/team/members/MemberHoverCard.tsx b/src/renderer/components/team/members/MemberHoverCard.tsx index 569d9951..0ed187ad 100644 --- a/src/renderer/components/team/members/MemberHoverCard.tsx +++ b/src/renderer/components/team/members/MemberHoverCard.tsx @@ -121,8 +121,14 @@ export const MemberHoverCard = ({ leadActivity: isLeadMember(member) ? leadActivity : undefined, }); const presenceLabel = launchPresentation.presenceLabel; + const launchVisualState = launchPresentation.launchVisualState; + const launchStatusLabel = launchPresentation.launchStatusLabel; const dotClass = launchPresentation.dotClass; const runtimeAdvisoryTitle = launchPresentation.runtimeAdvisoryTitle; + const badgeLabel = + launchVisualState === 'runtime_pending' || launchVisualState === 'permission_pending' + ? (launchStatusLabel ?? presenceLabel) + : presenceLabel; const currentTask: TeamTaskWithKanban | null = member.currentTaskId ? (tasks.find((t) => t.id === member.currentTaskId) ?? null) : null; @@ -172,7 +178,7 @@ export const MemberHoverCard = ({ border: `1px solid ${getThemedBorder(colors, isLight)}40`, }} > - {presenceLabel} + {badgeLabel} {roleLabel && ( diff --git a/test/renderer/components/team/members/MemberDetailHeader.test.ts b/test/renderer/components/team/members/MemberDetailHeader.test.ts index 0658e1a9..2cdfdd8d 100644 --- a/test/renderer/components/team/members/MemberDetailHeader.test.ts +++ b/test/renderer/components/team/members/MemberDetailHeader.test.ts @@ -100,6 +100,36 @@ describe('MemberDetailHeader spawn-aware presence', () => { }); }); + it('shows connecting while the runtime is online but bootstrap is still pending', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + React.createElement(MemberDetailHeader, { + member, + isTeamAlive: true, + isTeamProvisioning: false, + spawnStatus: 'online', + spawnLaunchState: 'runtime_pending_bootstrap', + spawnRuntimeAlive: true, + spawnLivenessSource: 'process', + }) + ); + await Promise.resolve(); + }); + + expect(host.textContent).toContain('connecting'); + expect(host.textContent).not.toContain('online'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + it('shows runtime retry text after the teammate has already joined', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); diff --git a/test/renderer/components/team/members/MemberHoverCard.test.ts b/test/renderer/components/team/members/MemberHoverCard.test.ts index 23e66673..f9fe301d 100644 --- a/test/renderer/components/team/members/MemberHoverCard.test.ts +++ b/test/renderer/components/team/members/MemberHoverCard.test.ts @@ -144,7 +144,7 @@ describe('MemberHoverCard spawn-aware presence', () => { }); }); - it('keeps runtime-pending members in starting state while launch is still settling', async () => { + it('shows connecting for runtime-pending members while launch is still settling', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.progress = { runId: 'run-1', @@ -188,7 +188,42 @@ describe('MemberHoverCard spawn-aware presence', () => { await Promise.resolve(); }); - expect(host.textContent).toContain('starting'); + expect(host.textContent).toContain('connecting'); + expect(host.textContent).not.toContain('online'); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + + it('shows connecting while runtime is online but bootstrap is still pending outside launch settling', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + storeState.progress = null; + storeState.memberSpawnStatusesByTeam['northstar-core'].alice = { + status: 'online', + launchState: 'runtime_pending_bootstrap', + updatedAt: '2026-04-09T10:00:00.000Z', + runtimeAlive: true, + livenessSource: 'process', + }; + storeState.memberSpawnSnapshotsByTeam['northstar-core'] = undefined; + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + + await act(async () => { + root.render( + React.createElement(MemberHoverCard, { + name: 'alice', + children: React.createElement('button', { type: 'button' }, 'alice'), + }) + ); + await Promise.resolve(); + }); + + expect(host.textContent).toContain('connecting'); expect(host.textContent).not.toContain('online'); await act(async () => {