diff --git a/docs/design/evidence/task-702/after-1440-dark.png b/docs/design/evidence/task-702/after-1440-dark.png
new file mode 100644
index 0000000000..eaad7765de
Binary files /dev/null and b/docs/design/evidence/task-702/after-1440-dark.png differ
diff --git a/docs/design/evidence/task-702/after-1440-light-modal.png b/docs/design/evidence/task-702/after-1440-light-modal.png
new file mode 100644
index 0000000000..d1d0011125
Binary files /dev/null and b/docs/design/evidence/task-702/after-1440-light-modal.png differ
diff --git a/docs/design/evidence/task-702/after-1440-light.png b/docs/design/evidence/task-702/after-1440-light.png
new file mode 100644
index 0000000000..7593c5b16f
Binary files /dev/null and b/docs/design/evidence/task-702/after-1440-light.png differ
diff --git a/docs/design/evidence/task-702/after-390-dark.png b/docs/design/evidence/task-702/after-390-dark.png
new file mode 100644
index 0000000000..79234e2821
Binary files /dev/null and b/docs/design/evidence/task-702/after-390-dark.png differ
diff --git a/docs/design/evidence/task-702/after-390-light.png b/docs/design/evidence/task-702/after-390-light.png
new file mode 100644
index 0000000000..25243b6bfa
Binary files /dev/null and b/docs/design/evidence/task-702/after-390-light.png differ
diff --git a/docs/design/evidence/task-702/before-1440-dark.png b/docs/design/evidence/task-702/before-1440-dark.png
new file mode 100644
index 0000000000..0950653f42
Binary files /dev/null and b/docs/design/evidence/task-702/before-1440-dark.png differ
diff --git a/docs/design/evidence/task-702/before-1440-light.png b/docs/design/evidence/task-702/before-1440-light.png
new file mode 100644
index 0000000000..f2003182b0
Binary files /dev/null and b/docs/design/evidence/task-702/before-1440-light.png differ
diff --git a/docs/design/evidence/task-702/before-390-dark.png b/docs/design/evidence/task-702/before-390-dark.png
new file mode 100644
index 0000000000..87b40a8cb8
Binary files /dev/null and b/docs/design/evidence/task-702/before-390-dark.png differ
diff --git a/docs/design/evidence/task-702/before-390-light.png b/docs/design/evidence/task-702/before-390-light.png
new file mode 100644
index 0000000000..c34c7f8bf8
Binary files /dev/null and b/docs/design/evidence/task-702/before-390-light.png differ
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index 25680c0b90..9f1b151437 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -668,7 +668,8 @@
"agents_one": "{{count}} agent",
"agents_other": "{{count}} agents",
"boardOpen": "board {{count}} open",
- "backToPods": "Back to pods"
+ "backToPods": "Back to pods",
+ "invite": "Invite"
},
"team": {
"view": "View pod team",
diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json
index 3ffafea633..ab2e359cff 100644
--- a/frontend/src/i18n/locales/zh-CN.json
+++ b/frontend/src/i18n/locales/zh-CN.json
@@ -667,7 +667,8 @@
"agents_one": "{{count}}个智能体",
"agents_other": "{{count}}个智能体",
"boardOpen": "看板{{count}}项待办",
- "backToPods": "返回 Pod 列表"
+ "backToPods": "返回 Pod 列表",
+ "invite": "邀请"
},
"team": {
"view": "查看 Pod 团队",
diff --git a/frontend/src/v2/__tests__/V2ThreadHeaderInvite.test.tsx b/frontend/src/v2/__tests__/V2ThreadHeaderInvite.test.tsx
new file mode 100644
index 0000000000..21f60c1961
--- /dev/null
+++ b/frontend/src/v2/__tests__/V2ThreadHeaderInvite.test.tsx
@@ -0,0 +1,90 @@
+// @ts-nocheck
+// TASK-702: the pod header carries an Invite for members, opening the same
+// invite modal the inspector does. Hidden for a non-member and on 1:1 pods.
+import React from 'react';
+import { fireEvent, render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import V2Thread from '../components/V2Thread';
+import { AuthContext } from '../../context/AuthContext';
+
+jest.mock('../../context/SocketContext', () => ({
+ useSocket: () => ({ socket: null, connected: false }),
+}));
+
+jest.mock('axios', () => {
+ const mock = {
+ get: jest.fn(() => Promise.resolve({ data: {} })),
+ post: jest.fn(() => Promise.resolve({ data: {} })),
+ patch: jest.fn(),
+ delete: jest.fn(),
+ defaults: { baseURL: '', headers: { common: {} } },
+ interceptors: {
+ request: { use: jest.fn(), eject: jest.fn() },
+ response: { use: jest.fn(), eject: jest.fn() },
+ },
+ };
+ return { __esModule: true, default: mock, ...mock };
+});
+
+beforeAll(() => {
+ Element.prototype.scrollIntoView = jest.fn();
+});
+
+const authValue = {
+ currentUser: { _id: 'u1', username: 'alice' },
+ user: { _id: 'u1', username: 'alice' },
+ token: 't',
+ loading: false,
+ error: null,
+ isAuthenticated: true,
+ register: jest.fn(),
+ login: jest.fn(),
+ logout: jest.fn(),
+ updateProfile: jest.fn(),
+};
+
+const renderThread = ({ podType = 'chat', member = true, onOpenInvite = jest.fn() } = {}) => {
+ const detail = {
+ pod: { _id: 'pod-1', name: 'Launch Room', type: podType },
+ members: member
+ ? [{ _id: 'u1', username: 'alice', isBot: false }, { _id: 'u2', username: 'bob', isBot: false }]
+ : [{ _id: 'u2', username: 'bob', isBot: false }],
+ messages: [],
+ agents: [],
+ sendMessage: jest.fn(),
+ loading: false,
+ error: null,
+ refresh: jest.fn(),
+ };
+ render(
+
+
+
+
+ ,
+ );
+ return onOpenInvite;
+};
+
+describe('V2Thread header Invite', () => {
+ test('a member of a team pod sees Invite, and it opens the people tab', () => {
+ const onOpenInvite = renderThread();
+ fireEvent.click(screen.getByTestId('pod-header-invite'));
+ expect(onOpenInvite).toHaveBeenCalledWith('people');
+ });
+
+ test('a non-member does not see Invite', () => {
+ renderThread({ member: false });
+ expect(screen.queryByTestId('pod-header-invite')).not.toBeInTheDocument();
+ });
+
+ test.each(['agent-room', 'agent-dm'])('a %s pod does not show Invite', (podType) => {
+ renderThread({ podType });
+ expect(screen.queryByTestId('pod-header-invite')).not.toBeInTheDocument();
+ });
+
+ test('no invite handler, no Invite', () => {
+ renderThread({ onOpenInvite: null });
+ expect(screen.queryByTestId('pod-header-invite')).not.toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
index 0326b1dc6e..a83fc3fe73 100644
--- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
+++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
@@ -5262,4 +5262,12 @@ describe('pod board column scroll (TASK-698)', () => {
expect(focus).toContain('max-height: min(35vh, 260px)');
expect(focus).toContain('overflow-y: auto');
});
+
+ // TASK-702: on a phone the header Invite is glyph-only, or its label takes
+ // the pod name's width and the title truncates to a few characters.
+ test('the header Invite hides its label on a phone, keeping it for screen readers', () => {
+ const block = mediaBlockContaining(read('../v2.css'), '.v2-pod-header__invite span');
+ expect(block).toMatch(/\.v2-pod-header__invite span\s*\{[^}]*clip-path:\s*inset\(50%\)/);
+ expect(block).toMatch(/\.v2-pod-header__invite\s*\{[^}]*width:\s*28px/);
+ });
});
diff --git a/frontend/src/v2/components/V2Thread.tsx b/frontend/src/v2/components/V2Thread.tsx
index 22495b787c..f29bc81dec 100644
--- a/frontend/src/v2/components/V2Thread.tsx
+++ b/frontend/src/v2/components/V2Thread.tsx
@@ -1943,6 +1943,21 @@ const V2Thread: React.FC = ({ detail, firstRunVisible = false, in
))}
+ {/* TASK-702: the invite lived only in the inspector, where nobody
+ looked. Members see it here; onOpenInvite is already absent
+ on the 1:1 pod types. */}
+ {onOpenInvite && isPodMember && !isAgentDm && !isAgentRoom && (
+
+ )}
{onToggleInspector && (