);
diff --git a/src/renderer/components/team/kanban/KanbanTaskCard.tsx b/src/renderer/components/team/kanban/KanbanTaskCard.tsx
index ca34de0a..e86968a7 100644
--- a/src/renderer/components/team/kanban/KanbanTaskCard.tsx
+++ b/src/renderer/components/team/kanban/KanbanTaskCard.tsx
@@ -270,9 +270,7 @@ export const KanbanTaskCard = ({
{task.needsClarification ? (
diff --git a/src/renderer/components/team/tasks/TaskRow.tsx b/src/renderer/components/team/tasks/TaskRow.tsx
index 55f3baec..2fbb7b7e 100644
--- a/src/renderer/components/team/tasks/TaskRow.tsx
+++ b/src/renderer/components/team/tasks/TaskRow.tsx
@@ -14,7 +14,7 @@ export const TaskRow = ({ task }: TaskRowProps): React.JSX.Element => {
| {task.id} |
{task.subject} |
- {task.owner ?? 'Unassigned'} |
+ {task.owner ?? '\u2014'} |
{task.kanbanColumn && task.kanbanColumn in KANBAN_COLUMN_DISPLAY
? KANBAN_COLUMN_DISPLAY[task.kanbanColumn].label
diff --git a/src/renderer/components/ui/MemberSelect.tsx b/src/renderer/components/ui/MemberSelect.tsx
new file mode 100644
index 00000000..b99c68c5
--- /dev/null
+++ b/src/renderer/components/ui/MemberSelect.tsx
@@ -0,0 +1,203 @@
+import * as React from 'react';
+
+import { getTeamColorSet } from '@renderer/constants/teamColors';
+import { cn } from '@renderer/lib/utils';
+import { formatAgentRole } from '@renderer/utils/formatAgentRole';
+import { agentAvatarUrl, buildMemberColorMap } from '@renderer/utils/memberHelpers';
+import { Command as CommandPrimitive } from 'cmdk';
+import { Check, ChevronsUpDown } from 'lucide-react';
+
+import { Popover, PopoverContent, PopoverTrigger } from './popover';
+
+import type { ResolvedTeamMember } from '@shared/types';
+
+interface MemberSelectProps {
+ members: ResolvedTeamMember[];
+ value: string | null;
+ onChange: (value: string | null) => void;
+ placeholder?: string;
+ /** Show "Unassigned" option at the top of the list */
+ allowUnassigned?: boolean;
+ /** Size variant */
+ size?: 'sm' | 'md';
+ disabled?: boolean;
+ className?: string;
+}
+
+const UNASSIGNED_VALUE = '__unassigned__';
+
+export const MemberSelect = ({
+ members,
+ value,
+ onChange,
+ placeholder = 'Select member...',
+ allowUnassigned = false,
+ size = 'sm',
+ disabled = false,
+ className,
+}: MemberSelectProps): React.JSX.Element => {
+ const [open, setOpen] = React.useState(false);
+ const [search, setSearch] = React.useState('');
+ const listboxId = React.useId();
+
+ const colorMap = React.useMemo(() => buildMemberColorMap(members), [members]);
+ const selectedMember = React.useMemo(
+ () => (value ? members.find((m) => m.name === value) : null),
+ [members, value],
+ );
+
+ const avatarSize = size === 'md' ? 32 : 24;
+ const avatarClass = size === 'md' ? 'size-6' : 'size-5';
+ const textSize = size === 'md' ? 'text-xs' : 'text-[10px]';
+ const triggerHeight = size === 'md' ? 'h-9' : 'h-8';
+
+ const renderMemberInline = (member: ResolvedTeamMember): React.ReactNode => {
+ const resolvedColor = colorMap.get(member.name);
+ const colors = getTeamColorSet(resolvedColor ?? '');
+ return (
+
+
+
+ {member.name === 'team-lead' ? 'lead' : member.name}
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ e.stopPropagation()}
+ >
+
+ No members found.
+
+ {allowUnassigned && !search.trim() ? (
+ {
+ onChange(null);
+ setOpen(false);
+ setSearch('');
+ }}
+ className="relative flex w-full cursor-default select-none items-center rounded-sm px-2 py-1.5 text-xs outline-none data-[selected=true]:bg-[var(--color-surface-raised)] data-[selected=true]:text-[var(--color-text)]"
+ >
+ Unassigned
+ {value === null ? (
+
+ ) : null}
+
+ ) : null}
+ {members
+ .filter((m) => {
+ if (!search.trim()) return true;
+ const q = search.toLowerCase();
+ return (
+ m.name.toLowerCase().includes(q) ||
+ (m.role?.toLowerCase().includes(q) ?? false) ||
+ (m.agentType?.toLowerCase().includes(q) ?? false)
+ );
+ })
+ .map((m) => {
+ const isSelected = m.name === value;
+ const resolvedColor = colorMap.get(m.name);
+ const colors = getTeamColorSet(resolvedColor ?? '');
+ const role = formatAgentRole(m.role) ?? formatAgentRole(m.agentType);
+
+ return (
+ {
+ onChange(m.name);
+ setOpen(false);
+ setSearch('');
+ }}
+ className="relative flex w-full cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none data-[selected=true]:bg-[var(--color-surface-raised)] data-[selected=true]:text-[var(--color-text)]"
+ >
+
+
+ {m.name === 'team-lead' ? 'lead' : m.name}
+
+ {role ? (
+
+ {role}
+
+ ) : null}
+ {isSelected ? (
+
+ ) : null}
+
+ );
+ })}
+
+
+
+
+ );
+};
diff --git a/test/main/ipc/teams.test.ts b/test/main/ipc/teams.test.ts
index 48ed8954..522dd469 100644
--- a/test/main/ipc/teams.test.ts
+++ b/test/main/ipc/teams.test.ts
@@ -8,58 +8,11 @@ vi.mock('electron', () => ({
BrowserWindow: { getAllWindows: vi.fn(() => []) },
}));
-vi.mock('@preload/constants/ipcChannels', () => ({
- TEAM_LIST: 'team:list',
- TEAM_GET_DATA: 'team:getData',
- TEAM_DELETE_TEAM: 'team:deleteTeam',
- TEAM_PREPARE_PROVISIONING: 'team:prepareProvisioning',
- TEAM_CREATE: 'team:create',
- TEAM_LAUNCH: 'team:launch',
- TEAM_CREATE_CONFIG: 'team:createConfig',
- TEAM_CREATE_TASK: 'team:createTask',
- TEAM_PROVISIONING_STATUS: 'team:provisioningStatus',
- TEAM_CANCEL_PROVISIONING: 'team:cancelProvisioning',
- TEAM_PROVISIONING_PROGRESS: 'team:provisioningProgress',
- TEAM_SEND_MESSAGE: 'team:sendMessage',
- TEAM_REQUEST_REVIEW: 'team:requestReview',
- TEAM_UPDATE_KANBAN: 'team:updateKanban',
- TEAM_UPDATE_KANBAN_COLUMN_ORDER: 'team:updateKanbanColumnOrder',
- TEAM_UPDATE_TASK_STATUS: 'team:updateTaskStatus',
- TEAM_UPDATE_TASK_FIELDS: 'team:updateTaskFields',
- TEAM_UPDATE_TASK_OWNER: 'team:updateTaskOwner',
- TEAM_PROCESS_SEND: 'team:processSend',
- TEAM_PROCESS_ALIVE: 'team:processAlive',
- TEAM_ALIVE_LIST: 'team:aliveList',
- TEAM_STOP: 'team:stop',
- TEAM_GET_MEMBER_LOGS: 'team:getMemberLogs',
- TEAM_GET_LOGS_FOR_TASK: 'team:getLogsForTask',
- TEAM_GET_MEMBER_STATS: 'team:getMemberStats',
- TEAM_UPDATE_CONFIG: 'team:updateConfig',
- TEAM_START_TASK: 'team:startTask',
- TEAM_GET_ALL_TASKS: 'team:getAllTasks',
- TEAM_ADD_TASK_COMMENT: 'team:addTaskComment',
- TEAM_ADD_MEMBER: 'team:addMember',
- TEAM_REPLACE_MEMBERS: 'team:replaceMembers',
- TEAM_REMOVE_MEMBER: 'team:removeMember',
- TEAM_UPDATE_MEMBER_ROLE: 'team:updateMemberRole',
- TEAM_GET_PROJECT_BRANCH: 'team:getProjectBranch',
- TEAM_GET_ATTACHMENTS: 'team:getAttachments',
- TEAM_KILL_PROCESS: 'team:killProcess',
- TEAM_LEAD_ACTIVITY: 'team:leadActivity',
- TEAM_LEAD_CONTEXT: 'team:leadContext',
- TEAM_SOFT_DELETE_TASK: 'team:softDeleteTask',
- TEAM_GET_DELETED_TASKS: 'team:getDeletedTasks',
- TEAM_SET_TASK_CLARIFICATION: 'team:setTaskClarification',
- TEAM_SHOW_MESSAGE_NOTIFICATION: 'team:showMessageNotification',
- TEAM_ADD_TASK_RELATIONSHIP: 'team:addTaskRelationship',
- TEAM_REMOVE_TASK_RELATIONSHIP: 'team:removeTaskRelationship',
- TEAM_RESTORE: 'team:restoreTeam',
- TEAM_PERMANENTLY_DELETE: 'team:permanentlyDeleteTeam',
- TEAM_RESTORE_TASK: 'team:restoreTask',
- TEAM_SAVE_TASK_ATTACHMENT: 'team:saveTaskAttachment',
- TEAM_GET_TASK_ATTACHMENT: 'team:getTaskAttachment',
- TEAM_DELETE_TASK_ATTACHMENT: 'team:deleteTaskAttachment',
-}));
+// Keep this mock resilient to new exports (avoid drift).
+vi.mock('@preload/constants/ipcChannels', async (importOriginal) => {
+ const actual = await importOriginal();
+ return { ...actual };
+});
import {
TEAM_ALIVE_LIST,
|