;
+
+const makePod = (id: string, memberIds: string[]) => ({
+ _id: id,
+ name: id === COMMUNITY_POD_ID ? 'Commonly HQ' : 'My Workspace',
+ type: 'team',
+ members: memberIds.map((_id) => ({ _id, username: _id, isBot: false })),
+ createdAt: '2026-07-21T12:00:00.000Z',
+ updatedAt: '2026-07-21T12:00:00.000Z',
+});
+
+const renderSidebar = (pods) => {
+ const podsState = {
+ pods,
+ loading: false,
+ error: null,
+ createPod: jest.fn(),
+ patchLastMessage: jest.fn(),
+ };
+ return render(
+
+
+
+ ,
+ );
+};
+
+describe('V2PodsSidebar Community offer', () => {
+ const originalCommunityPodId = process.env.REACT_APP_COMMUNITY_POD_ID;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ process.env.REACT_APP_COMMUNITY_POD_ID = COMMUNITY_POD_ID;
+ });
+
+ afterAll(() => {
+ if (originalCommunityPodId === undefined) {
+ delete process.env.REACT_APP_COMMUNITY_POD_ID;
+ } else {
+ process.env.REACT_APP_COMMUNITY_POD_ID = originalCommunityPodId;
+ }
+ });
+
+ test('shows for a configured Community pod the human has not joined and navigates to the redirect', () => {
+ renderSidebar([makePod('workspace', ['human-1'])]);
+
+ expect(screen.getByText('Meet the builders and their agents.')).toBeInTheDocument();
+ fireEvent.click(screen.getByRole('button', { name: 'Join HQ' }));
+ expect(screen.getByTestId('current-path')).toHaveTextContent('/v2/community');
+ });
+
+ test('stays hidden once the Community pod is in memberPodIds', () => {
+ renderSidebar([
+ makePod('workspace', ['human-1']),
+ makePod(COMMUNITY_POD_ID, ['human-1']),
+ ]);
+
+ expect(screen.queryByRole('button', { name: 'Join HQ' })).not.toBeInTheDocument();
+ });
+
+ test('stays hidden for self-hosted instances without Community config', () => {
+ delete process.env.REACT_APP_COMMUNITY_POD_ID;
+ renderSidebar([makePod('workspace', ['human-1'])]);
+
+ expect(screen.queryByRole('button', { name: 'Join HQ' })).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 06024233..8fc4ee4e 100644
--- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
+++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
@@ -115,4 +115,23 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(rule).toContain('background: var(--v2-accent)');
expect(rule).toContain('color: #fff');
});
+
+ test('the Community offer stays visible below the independently scrolling pod list', () => {
+ // The list owns overflow-y; the offer is its flex sibling. Preventing the
+ // card from shrinking is what keeps the Join HQ action reachable when the
+ // sidebar contains many pods.
+ expect(ruleBody(v2, '.v2-pods__community')).toContain('flex-shrink: 0');
+ });
+
+ test('starter prompts wrap within the mobile chat pane', () => {
+ // At 390px the rail leaves a narrow main pane. Both the row and each chip
+ // need explicit shrink/wrap rules or the longest prompt creates horizontal
+ // overflow and pushes the composer action off-screen.
+ const row = ruleBody(v2, '.v2-chat__starter-prompts');
+ const chip = ruleBody(v2, '.v2-root button.v2-chat__starter-prompt');
+ expect(row).toContain('flex-wrap: wrap');
+ expect(row).toContain('max-width: 100%');
+ expect(chip).toContain('max-width: 100%');
+ expect(chip).toContain('white-space: normal');
+ });
});
diff --git a/frontend/src/v2/components/V2PodChat.tsx b/frontend/src/v2/components/V2PodChat.tsx
index 31abf207..26b89418 100644
--- a/frontend/src/v2/components/V2PodChat.tsx
+++ b/frontend/src/v2/components/V2PodChat.tsx
@@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
import V2Avatar from './V2Avatar';
import V2MessageBubble from './V2MessageBubble';
import {
@@ -14,6 +13,12 @@ import { initialsFor } from '../utils/avatars';
const PLAN_MODE_KEY = 'v2.podMode';
+const STARTER_PROMPTS = [
+ 'Introduce yourself — what are you best at?',
+ "Here's what I'm working on — where can you help?",
+ 'What should I ask you first?',
+] as const;
+
type PodMode = 'plan' | 'execute';
const podMarkFor = (name: string, type?: string): string => (
@@ -150,7 +155,6 @@ const Icon = ({ d }: { d: string }) => (
const V2PodChat: React.FC = ({ detail, firstRunHero, firstRunVisible = false, inspectorCollapsed, onToggleInspector, onOpenMember, onOpenInvite, onOpenFile, onOpenMobileNav }) => {
const { pod, members, messages, agents, sendMessage, loading, error } = detail;
- const navigate = useNavigate();
const api = useV2Api();
const { socket, connected } = useSocket();
const { currentUser } = useAuth();
@@ -560,6 +564,17 @@ const V2PodChat: React.FC = ({ detail, firstRunHero, firstRunVis
}
};
+ const handleStarterPrompt = (prompt: string) => {
+ setDraft(prompt);
+ setMentionOpen(false);
+ requestAnimationFrame(() => {
+ const input = composerInputRef.current;
+ if (!input) return;
+ input.focus();
+ input.setSelectionRange(prompt.length, prompt.length);
+ });
+ };
+
// Composer attach: handles both images (sends as standalone image message,
// legacy v2 behavior) and other file kinds (PDF / md / txt / csv / json,
// inserts an [[upload:fileName|originalName|size|kind]] directive into the
@@ -728,39 +743,16 @@ const V2PodChat: React.FC = ({ detail, firstRunHero, firstRunVis
They'll DM each other when one of them needs the other's help.
>
) : isAgentRoom && botMembers.length === 1 ? (
- // Sprint B4: first-message coaching for the 60s "install
- // your first agent → talk to it" wedge. Shows the agent's
- // display name + 3 generic suggestion chips that pre-fill
- // the composer. Pod-summarizer agent-rooms ride this same
- // branch — generic chips degrade gracefully for those.
(() => {
const rawUsername = botMembers[0]?.username || '';
const agentName = agentDisplayNames.get(rawUsername.toLowerCase())
|| rawUsername
|| 'agent';
- const suggestions = [
- `Hey ${agentName}, what can you do for me?`,
- 'What are you working on right now?',
- 'Help me get started — what should I try first?',
- ];
return (
<>
Say hi to {agentName}
- This is your 1:1 with {agentName}. Try one of these, or write your own:
-
-
- {suggestions.map((s) => (
-
- ))}
+ This is your private 1:1. Choose a prompt below, or write your own.
This is a private 1:1 conversation. Say hello to get started.
>
- ) : (members || []).length <= 1 && botMembers.length === 0 ? (
- // Starter workspace: the user is alone in an empty pod
- // (the default "My Workspace" every signup gets, or any
- // solo pod). Scripted onboarding — the checklist tasks
- // seeded at signup live on the board; this points the
- // way to the first one.
+ ) : (
<>
-
Welcome to your workspace
+
This pod is quiet
- This is where you and your agents work together. Agents you
- connect keep their memory here — everything they learn stays
- with them. Start with the checklist on your task board, or
- jump straight in:
-
-
-
- {/* Points at Your Team (not the marketplace) while the
- marketplace sits behind its coming-soon wall — a
- fresh user's second CTA must not be a dead end. */}
-
+ Use @ to mention an agent or teammate—everyone in the member list can see and reply.
>
- ) : (
- <>
-
Talk to your team
-
Type a message, or @-mention an agent to direct your first task.