From cdb2c8b29c7315dbc72b767298fdf2240665b992 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 00:03:24 -0700 Subject: [PATCH 1/2] fix(inspector): Needs you lists this pod's queue; Who can join shows for members (TASK-742) The workspace queue is a global newest-first slice, so filtering it to the open pod could come back empty while the per-pod count was not. The inspector now asks the decision queue for this pod's rows (limit 50) and falls back to the shared slice only if that read fails. Who can join was gated to the owner, so members saw no join section at all. Members now see the current policy as read-only text; only the owner gets the segment. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: Polish Agent-Model: claude/claude-haiku-5-5/default --- .../src/v2/__tests__/V2Inspector.test.tsx | 18 +++++++++++++++ frontend/src/v2/components/V2Inspector.tsx | 22 ++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/frontend/src/v2/__tests__/V2Inspector.test.tsx b/frontend/src/v2/__tests__/V2Inspector.test.tsx index 9a9840d16..d37c061d3 100644 --- a/frontend/src/v2/__tests__/V2Inspector.test.tsx +++ b/frontend/src/v2/__tests__/V2Inspector.test.tsx @@ -392,6 +392,24 @@ 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(); + }); + 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. */} Date: Sun, 1 Nov 2026 00:19:24 -0700 Subject: [PATCH 2/2] test(inspector): cover rejected queue fallback and member join text (TASK-742) Co-Authored-By: Claude Haiku 5.5 Co-authored-by: Polish Agent-Model: claude/claude-haiku-5-5/default --- frontend/src/v2/__tests__/V2Inspector.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/v2/__tests__/V2Inspector.test.tsx b/frontend/src/v2/__tests__/V2Inspector.test.tsx index d37c061d3..3359e2d2f 100644 --- a/frontend/src/v2/__tests__/V2Inspector.test.tsx +++ b/frontend/src/v2/__tests__/V2Inspector.test.tsx @@ -408,6 +408,15 @@ describe('V2Inspector', () => { }); 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 () => {