Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions frontend/src/v2/__tests__/V2Inspector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }] });
Expand Down
22 changes: 19 additions & 3 deletions frontend/src/v2/components/V2Inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,20 @@ const V2Inspector: React.FC<V2InspectorProps> = ({ 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<V2AttentionItem[] | null>(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(() => {
Expand Down Expand Up @@ -469,7 +480,12 @@ const V2Inspector: React.FC<V2InspectorProps> = ({ detail, currentUserId, onPodC
</div>
))}

{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.
<p className="v2-pods__join-policy-hint">
{joinPolicy === 'open' ? t('inspector.workspace.joinOpen') : t('podsSidebar.create.privateOption')}
</p>
) : (
<div className="v2-pod-join-settings">
{/* D215: two exclusive values read better side by side than as radios. */}
<V2Segmented
Expand Down
Loading