diff --git a/frontend/src/v2/__tests__/useV2ThreadMentions.test.tsx b/frontend/src/v2/__tests__/useV2ThreadMentions.test.tsx
new file mode 100644
index 0000000000..f4f77aaa1e
--- /dev/null
+++ b/frontend/src/v2/__tests__/useV2ThreadMentions.test.tsx
@@ -0,0 +1,74 @@
+// @ts-nocheck
+// The @ picker (C14 rehearsal, 2026-10-20): typing "@eng" listed the person's
+// own account first and the agent second, so Enter mentioned themself and the
+// agent never woke. The picker never offers your own handle, and among matches
+// it puts prefix matches first and agents before people.
+import React from 'react';
+import { renderHook, act } from '@testing-library/react';
+import { AuthContext } from '../../context/AuthContext';
+import { useV2ThreadMentions } from '../hooks/useV2ThreadMentions';
+
+const auth = (username) => ({
+ currentUser: { _id: 'u-self', username }, user: { _id: 'u-self', username }, token: 't', loading: false,
+ error: null, isAuthenticated: true, register: jest.fn(), login: jest.fn(), logout: jest.fn(), updateProfile: jest.fn(),
+});
+
+const members = [
+ { _id: 'u-self', username: 'laruiss' },
+ { _id: 'u-other', username: 'larissa' },
+ { _id: 'u-agnes', username: 'agnes' },
+ { _id: 'b-1', username: 'laruiss-agent' },
+];
+const agents = [{ agentName: 'laruiss-agent', instanceId: 'default', displayName: 'laruiss-agent' }];
+
+const open = (query, username = 'laruiss', podMembers = members) => {
+ const draft = `@${query}`;
+ const inputRef = { current: { selectionStart: draft.length, focus: jest.fn(), setSelectionRange: jest.fn() } };
+ const wrapper = ({ children }) => {children};
+ const { result } = renderHook(() => useV2ThreadMentions({
+ members: podMembers, agents, agentStateByHandle: new Map(), draft, setDraft: jest.fn(), inputRef,
+ }), { wrapper });
+ act(() => { result.current.updateMentionState(draft, draft.length); });
+ return result.current.filteredMentions.map((item) => item.value || item.label);
+};
+
+describe('@ picker', () => {
+ test('never offers your own handle', () => {
+ expect(open('lar')).not.toContain('laruiss');
+ });
+
+ test('the agent comes first, so Enter picks it', () => {
+ expect(open('lar')[0]).toBe('laruiss-agent');
+ });
+
+ test('other people are still offered, after the agent', () => {
+ const list = open('lar');
+ expect(list).toContain('larissa');
+ expect(list.indexOf('laruiss-agent')).toBeLessThan(list.indexOf('larissa'));
+ });
+
+ test('a prefix match outranks a match in the middle, even a person over an agent', () => {
+ // "ag" is a prefix of agnes and only inside laruiss-agent: the person
+ // typing "@ag" means agnes.
+ const list = open('ag');
+ expect(list).toEqual(expect.arrayContaining(['agnes', 'laruiss-agent']));
+ expect(list[0]).toBe('agnes');
+ });
+
+ // sprint-review on #2366: with more matches than the picker shows, ranking
+ // must happen before the cap, or an agent late in pod order drops out.
+ test('ranking happens before the 8-item cap: the agent last in pod order still leads', () => {
+ const crowd = Array.from({ length: 10 }, (_, i) => ({ _id: `u-${i}`, username: `lar${i}` }));
+ const list = open('lar', 'laruiss', [...crowd, { _id: 'b-1', username: 'laruiss-agent' }]);
+ expect(list).toHaveLength(8);
+ expect(list[0]).toBe('laruiss-agent');
+ });
+
+ test('the self-skip ignores case', () => {
+ expect(open('lar', 'LaRuiss')).not.toContain('laruiss');
+ });
+
+ test('someone else signed in sees laruiss as a member', () => {
+ expect(open('lar', 'larissa')).toContain('laruiss');
+ });
+});
diff --git a/frontend/src/v2/hooks/useV2ThreadMentions.ts b/frontend/src/v2/hooks/useV2ThreadMentions.ts
index ea77845363..94e242e93e 100644
--- a/frontend/src/v2/hooks/useV2ThreadMentions.ts
+++ b/frontend/src/v2/hooks/useV2ThreadMentions.ts
@@ -1,5 +1,6 @@
import { useCallback, useMemo, useState, type RefObject } from 'react';
import { useTranslation } from 'react-i18next';
+import { useAuth } from '../../context/AuthContext';
import type { V2Agent } from './useV2PodDetail';
import type { V2PodMember } from './useV2Pods';
import type { V2ComposerMention, V2ComposerWarning } from '../components/V2Composer';
@@ -53,6 +54,8 @@ export const useV2ThreadMentions = ({
inputRef,
}: UseV2ThreadMentionsArgs) => {
const { t } = useTranslation();
+ const { currentUser } = useAuth();
+ const selfUsername = (currentUser?.username || '').toLowerCase();
const [mentionOpen, setMentionOpen] = useState(false);
const [mentionQuery, setMentionQuery] = useState('');
const [mentionStart, setMentionStart] = useState(-1);
@@ -99,6 +102,10 @@ export const useV2ThreadMentions = ({
const username = member.username || '';
if (!username) return;
const key = username.toLowerCase();
+ // Never offer the person their own handle. It was the first suggestion
+ // for any prefix of their name, so Enter mentioned themself and the
+ // agent they meant never woke (C14 rehearsal, 2026-10-20).
+ if (selfUsername && key === selfUsername) return;
if (seen.has(key)) return;
seen.add(key);
const agent = agentByUsername.get(key);
@@ -128,7 +135,7 @@ export const useV2ThreadMentions = ({
if (item) items.push(item);
});
return items;
- }, [agentStateByHandle, agents, members, t]);
+ }, [agentStateByHandle, agents, members, selfUsername, t]);
const warnings = useMemo(() => {
if (!draft.includes('@')) return [];
@@ -149,7 +156,20 @@ export const useV2ThreadMentions = ({
const filteredMentions = useMemo(() => {
if (!mentionOpen) return [];
const query = mentionQuery.trim().toLowerCase();
- return mentionableItems.filter((item) => item.labelLower.includes(query)).slice(0, 8);
+ // Prefix matches first, and agents before people among equals, so Enter
+ // lands on the agent the person is most likely addressing. Otherwise the
+ // pod's own order holds.
+ const prefix = (item: MentionItem) => (
+ (item.value || '').toLowerCase().startsWith(query) || item.label.toLowerCase().startsWith(query)
+ );
+ return mentionableItems
+ .map((item, order) => ({ item, order }))
+ .filter(({ item }) => item.labelLower.includes(query))
+ .sort((a, b) => (Number(prefix(b.item)) - Number(prefix(a.item)))
+ || (Number(b.item.isAgent) - Number(a.item.isAgent))
+ || (a.order - b.order))
+ .map(({ item }) => item)
+ .slice(0, 8);
}, [mentionOpen, mentionQuery, mentionableItems]);
const updateMentionState = useCallback((nextValue: string, cursorPosition: number | null) => {