diff --git a/frontend/src/v2/__tests__/V2Inspector.test.tsx b/frontend/src/v2/__tests__/V2Inspector.test.tsx index 9a9840d16..3359e2d2f 100644 --- a/frontend/src/v2/__tests__/V2Inspector.test.tsx +++ b/frontend/src/v2/__tests__/V2Inspector.test.tsx @@ -392,6 +392,33 @@ describe('V2Inspector', () => { expect(mockNavigate).toHaveBeenCalledWith('/v2/agents'); }); + test('lists this pod\'s own queue rows even when the global slice misses them', async () => { + mockGet.mockImplementation((url: string) => Promise.resolve(url.startsWith('/api/activity/decision-queue?podId=pod-1') + ? { items: [{ id: 'pod-row', kind: 'decision', title: 'Pod-only decision', podId: 'pod-1' }] } + : { tasks: [] })); + renderInspector({ attentionCount: 1, attentionItems: [] }); + expect(await screen.findByText('Pod-only decision')).toBeInTheDocument(); + expect(mockGet).toHaveBeenCalledWith('/api/activity/decision-queue?podId=pod-1&limit=50'); + }); + + test('members see the join policy as read-only text, not a segment', () => { + renderInspector({ + currentUserId: 'another-user', + detail: { ...detail, pod: { ...detail.pod, createdBy: { _id: 'owner-1' }, joinPolicy: 'invite-only' } } as any, + }); + expect(screen.queryByRole('radiogroup', { name: 'Who can join' })).not.toBeInTheDocument(); + expect(screen.getByRole('heading', { name: /Who can join/ })).toBeInTheDocument(); + expect(screen.getByText('Invite-only')).toBeInTheDocument(); + }); + + test('falls back to the shared queue slice when this pod\'s queue read is rejected', async () => { + mockGet.mockImplementation((url: string) => (url.startsWith('/api/activity/decision-queue') + ? Promise.reject(new Error('down')) + : Promise.resolve({ tasks: [] }))); + renderInspector({ attentionCount: 1, attentionItems: [{ id: 'shared', kind: 'decision', title: 'Shared slice item', podId: 'pod-1' }] }); + expect(await screen.findByText('Shared slice item')).toBeInTheDocument(); + }); + test('does not render a stale attention item from another pod', async () => { mockGet.mockResolvedValue({ tasks: [] }); renderInspector({ attentionCount: 0, attentionItems: [{ id: 'other', kind: 'decision', title: 'Other pod', podId: 'pod-2' }] }); diff --git a/frontend/src/v2/components/V2Inspector.tsx b/frontend/src/v2/components/V2Inspector.tsx index 607bc66c3..b1e7f0207 100644 --- a/frontend/src/v2/components/V2Inspector.tsx +++ b/frontend/src/v2/components/V2Inspector.tsx @@ -184,9 +184,20 @@ const V2Inspector: React.FC = ({ detail, currentUserId, onPodC setJoinPolicy(pod?.joinPolicy === 'invite-only' ? 'invite-only' : 'open'); setPolicyError(null); }, [pod?._id, pod?.joinPolicy]); + // The workspace queue is a global newest-first slice, so filtering it to this pod + // can come back empty while the per-pod count is not. Ask for this pod's own rows. + const [podAttention, setPodAttention] = useState(null); + useEffect(() => { + if (!pod?._id) { setPodAttention(null); return undefined; } + let active = true; + api.get<{ items?: V2AttentionItem[] }>(`/api/activity/decision-queue?podId=${encodeURIComponent(pod._id)}&limit=50`) + .then((data) => { if (active) setPodAttention(Array.isArray(data?.items) ? data.items : null); }) + .catch(() => { if (active) setPodAttention(null); }); + return () => { active = false; }; + }, [api, pod?._id, attentionCount]); const attention = useMemo( - () => attentionItems.filter((item) => item.podId === pod?._id), - [attentionItems, pod?._id], + () => podAttention ?? attentionItems.filter((item) => item.podId === pod?._id), + [podAttention, attentionItems, pod?._id], ); useEffect(() => { @@ -469,7 +480,12 @@ const V2Inspector: React.FC = ({ detail, currentUserId, onPodC ))} - {isOwner && !['agent-room', 'agent-dm', 'agent-admin'].includes(String(pod.type)) && renderGroup('join', 'lock', t('podsSidebar.create.whoCanJoin'), null, ( + {!['agent-room', 'agent-dm', 'agent-admin'].includes(String(pod.type)) && renderGroup('join', 'lock', t('podsSidebar.create.whoCanJoin'), null, !isOwner ? ( + // Members see the policy; only the owner can change it. +

+ {joinPolicy === 'open' ? t('inspector.workspace.joinOpen') : t('podsSidebar.create.privateOption')} +

+ ) : (
{/* D215: two exclusive values read better side by side than as radios. */}