diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 01f210ed..c017306c 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -10,6 +10,11 @@ import { assetsApi } from "../api/assets"; import { queryKeys } from "../lib/queryKeys"; import { useProjectOrder } from "../hooks/useProjectOrder"; import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; +import { + assigneeValueFromSelection, + currentUserAssigneeOption, + parseAssigneeValue, +} from "../lib/assignees"; import { Dialog, DialogContent, @@ -63,7 +68,8 @@ interface IssueDraft { description: string; status: string; priority: string; - assigneeId: string; + assigneeValue: string; + assigneeId?: string; projectId: string; assigneeModelOverride: string; assigneeThinkingEffort: string; @@ -173,7 +179,7 @@ export function NewIssueDialog() { const [description, setDescription] = useState(""); const [status, setStatus] = useState("todo"); const [priority, setPriority] = useState(""); - const [assigneeId, setAssigneeId] = useState(""); + const [assigneeValue, setAssigneeValue] = useState(""); const [projectId, setProjectId] = useState(""); const [assigneeOptionsOpen, setAssigneeOptionsOpen] = useState(false); const [assigneeModelOverride, setAssigneeModelOverride] = useState(""); @@ -220,7 +226,11 @@ export function NewIssueDialog() { userId: currentUserId, }); - const assigneeAdapterType = (agents ?? []).find((agent) => agent.id === assigneeId)?.adapterType ?? null; + const selectedAssignee = useMemo(() => parseAssigneeValue(assigneeValue), [assigneeValue]); + const selectedAssigneeAgentId = selectedAssignee.assigneeAgentId; + const selectedAssigneeUserId = selectedAssignee.assigneeUserId; + + const assigneeAdapterType = (agents ?? []).find((agent) => agent.id === selectedAssigneeAgentId)?.adapterType ?? null; const supportsAssigneeOverrides = Boolean( assigneeAdapterType && ISSUE_OVERRIDE_ADAPTER_TYPES.has(assigneeAdapterType), ); @@ -295,7 +305,7 @@ export function NewIssueDialog() { description, status, priority, - assigneeId, + assigneeValue, projectId, assigneeModelOverride, assigneeThinkingEffort, @@ -307,7 +317,7 @@ export function NewIssueDialog() { description, status, priority, - assigneeId, + assigneeValue, projectId, assigneeModelOverride, assigneeThinkingEffort, @@ -330,7 +340,7 @@ export function NewIssueDialog() { setStatus(newIssueDefaults.status ?? "todo"); setPriority(newIssueDefaults.priority ?? ""); setProjectId(newIssueDefaults.projectId ?? ""); - setAssigneeId(newIssueDefaults.assigneeAgentId ?? ""); + setAssigneeValue(assigneeValueFromSelection(newIssueDefaults)); setAssigneeModelOverride(""); setAssigneeThinkingEffort(""); setAssigneeChrome(false); @@ -340,7 +350,11 @@ export function NewIssueDialog() { setDescription(draft.description); setStatus(draft.status || "todo"); setPriority(draft.priority); - setAssigneeId(newIssueDefaults.assigneeAgentId ?? draft.assigneeId); + setAssigneeValue( + newIssueDefaults.assigneeAgentId || newIssueDefaults.assigneeUserId + ? assigneeValueFromSelection(newIssueDefaults) + : (draft.assigneeValue ?? draft.assigneeId ?? ""), + ); setProjectId(newIssueDefaults.projectId ?? draft.projectId); setAssigneeModelOverride(draft.assigneeModelOverride ?? ""); setAssigneeThinkingEffort(draft.assigneeThinkingEffort ?? ""); @@ -350,7 +364,7 @@ export function NewIssueDialog() { setStatus(newIssueDefaults.status ?? "todo"); setPriority(newIssueDefaults.priority ?? ""); setProjectId(newIssueDefaults.projectId ?? ""); - setAssigneeId(newIssueDefaults.assigneeAgentId ?? ""); + setAssigneeValue(assigneeValueFromSelection(newIssueDefaults)); setAssigneeModelOverride(""); setAssigneeThinkingEffort(""); setAssigneeChrome(false); @@ -390,7 +404,7 @@ export function NewIssueDialog() { setDescription(""); setStatus("todo"); setPriority(""); - setAssigneeId(""); + setAssigneeValue(""); setProjectId(""); setAssigneeOptionsOpen(false); setAssigneeModelOverride(""); @@ -406,7 +420,7 @@ export function NewIssueDialog() { function handleCompanyChange(companyId: string) { if (companyId === effectiveCompanyId) return; setDialogCompanyId(companyId); - setAssigneeId(""); + setAssigneeValue(""); setProjectId(""); setAssigneeModelOverride(""); setAssigneeThinkingEffort(""); @@ -443,7 +457,8 @@ export function NewIssueDialog() { description: description.trim() || undefined, status, priority: priority || "medium", - ...(assigneeId ? { assigneeAgentId: assigneeId } : {}), + ...(selectedAssigneeAgentId ? { assigneeAgentId: selectedAssigneeAgentId } : {}), + ...(selectedAssigneeUserId ? { assigneeUserId: selectedAssigneeUserId } : {}), ...(projectId ? { projectId } : {}), ...(assigneeAdapterOverrides ? { assigneeAdapterOverrides } : {}), ...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}), @@ -475,7 +490,9 @@ export function NewIssueDialog() { const hasDraft = title.trim().length > 0 || description.trim().length > 0; const currentStatus = statuses.find((s) => s.value === status) ?? statuses[1]!; const currentPriority = priorities.find((p) => p.value === priority); - const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId); + const currentAssignee = selectedAssigneeAgentId + ? (agents ?? []).find((a) => a.id === selectedAssigneeAgentId) + : null; const currentProject = orderedProjects.find((project) => project.id === projectId); const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI ? currentProject?.executionWorkspacePolicy ?? null @@ -497,16 +514,18 @@ export function NewIssueDialog() { : ISSUE_THINKING_EFFORT_OPTIONS.claude_local; const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [newIssueOpen]); const assigneeOptions = useMemo( - () => - sortAgentsByRecency( + () => [ + ...currentUserAssigneeOption(currentUserId), + ...sortAgentsByRecency( (agents ?? []).filter((agent) => agent.status !== "terminated"), recentAssigneeIds, ).map((agent) => ({ - id: agent.id, + id: assigneeValueFromSelection({ assigneeAgentId: agent.id }), label: agent.name, searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`, })), - [agents, recentAssigneeIds], + ], + [agents, currentUserId, recentAssigneeIds], ); const projectOptions = useMemo( () => @@ -710,7 +729,16 @@ export function NewIssueDialog() { } if (e.key === "Tab" && !e.shiftKey) { e.preventDefault(); - assigneeSelectorRef.current?.focus(); + if (assigneeValue) { + // Assignee already set — skip to project or description + if (projectId) { + descriptionEditorRef.current?.focus(); + } else { + projectSelectorRef.current?.focus(); + } + } else { + assigneeSelectorRef.current?.focus(); + } } }} autoFocus @@ -723,33 +751,49 @@ export function NewIssueDialog() { For { if (id) trackRecentAssignee(id); setAssigneeId(id); }} + onChange={(value) => { + const nextAssignee = parseAssigneeValue(value); + if (nextAssignee.assigneeAgentId) { + trackRecentAssignee(nextAssignee.assigneeAgentId); + } + setAssigneeValue(value); + }} onConfirm={() => { - projectSelectorRef.current?.focus(); + if (projectId) { + descriptionEditorRef.current?.focus(); + } else { + projectSelectorRef.current?.focus(); + } }} renderTriggerValue={(option) => - option && currentAssignee ? ( - <> - + option ? ( + currentAssignee ? ( + <> + + {option.label} + + ) : ( {option.label} - + ) ) : ( Assignee ) } renderOption={(option) => { if (!option.id) return {option.label}; - const assignee = (agents ?? []).find((agent) => agent.id === option.id); + const assignee = parseAssigneeValue(option.id).assigneeAgentId + ? (agents ?? []).find((agent) => agent.id === parseAssigneeValue(option.id).assigneeAgentId) + : null; return ( <> - + {assignee ? : null} {option.label} );