diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index d95c5f8d..ff7229dc 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { Link } from "@/lib/router"; import type { Issue } from "@paperclipai/shared"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -9,6 +9,7 @@ import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { queryKeys } from "../lib/queryKeys"; import { useProjectOrder } from "../hooks/useProjectOrder"; +import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; import { StatusIcon } from "./StatusIcon"; import { PriorityIcon } from "./PriorityIcon"; import { Identity } from "./Identity"; @@ -181,6 +182,12 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp return project ? projectUrl(project) : `/projects/${id}`; }; + const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [assigneeOpen]); + const sortedAgents = useMemo( + () => sortAgentsByRecency((agents ?? []).filter((a) => a.status !== "terminated"), recentAssigneeIds), + [agents, recentAssigneeIds], + ); + const assignee = issue.assigneeAgentId ? agents?.find((a) => a.id === issue.assigneeAgentId) : null; @@ -342,8 +349,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp {creatorUserLabel ? `Assign to ${creatorUserLabel === "Me" ? "me" : creatorUserLabel}` : "Assign to requester"} )} - {(agents ?? []) - .filter((a) => a.status !== "terminated") + {sortedAgents .filter((a) => { if (!assigneeSearch.trim()) return true; const q = assigneeSearch.toLowerCase(); @@ -356,7 +362,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp "flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50", a.id === issue.assigneeAgentId && "bg-accent" )} - onClick={() => { onUpdate({ assigneeAgentId: a.id, assigneeUserId: null }); setAssigneeOpen(false); }} + onClick={() => { trackRecentAssignee(a.id); onUpdate({ assigneeAgentId: a.id, assigneeUserId: null }); setAssigneeOpen(false); }} > {a.name} diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 1b07385f..6b872c28 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -10,6 +10,7 @@ import { authApi } from "../api/auth"; import { assetsApi } from "../api/assets"; import { queryKeys } from "../lib/queryKeys"; import { useProjectOrder } from "../hooks/useProjectOrder"; +import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees"; import { Dialog, DialogContent, @@ -472,16 +473,18 @@ export function NewIssueDialog() { : assigneeAdapterType === "opencode_local" ? ISSUE_THINKING_EFFORT_OPTIONS.opencode_local : ISSUE_THINKING_EFFORT_OPTIONS.claude_local; + const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [newIssueOpen]); const assigneeOptions = useMemo( () => - (agents ?? []) - .filter((agent) => agent.status !== "terminated") - .map((agent) => ({ - id: agent.id, - label: agent.name, - searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`, - })), - [agents], + sortAgentsByRecency( + (agents ?? []).filter((agent) => agent.status !== "terminated"), + recentAssigneeIds, + ).map((agent) => ({ + id: agent.id, + label: agent.name, + searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`, + })), + [agents, recentAssigneeIds], ); const projectOptions = useMemo( () => @@ -637,7 +640,7 @@ export function NewIssueDialog() { noneLabel="No assignee" searchPlaceholder="Search assignees..." emptyMessage="No assignees found." - onChange={setAssigneeId} + onChange={(id) => { if (id) trackRecentAssignee(id); setAssigneeId(id); }} onConfirm={() => { projectSelectorRef.current?.focus(); }} diff --git a/ui/src/lib/recent-assignees.ts b/ui/src/lib/recent-assignees.ts new file mode 100644 index 00000000..7c3e9c91 --- /dev/null +++ b/ui/src/lib/recent-assignees.ts @@ -0,0 +1,36 @@ +const STORAGE_KEY = "paperclip:recent-assignees"; +const MAX_RECENT = 10; + +export function getRecentAssigneeIds(): string[] { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +export function trackRecentAssignee(agentId: string): void { + if (!agentId) return; + const recent = getRecentAssigneeIds().filter((id) => id !== agentId); + recent.unshift(agentId); + if (recent.length > MAX_RECENT) recent.length = MAX_RECENT; + localStorage.setItem(STORAGE_KEY, JSON.stringify(recent)); +} + +export function sortAgentsByRecency( + agents: T[], + recentIds: string[], +): T[] { + const recentIndex = new Map(recentIds.map((id, i) => [id, i])); + return [...agents].sort((a, b) => { + const aRecent = recentIndex.get(a.id); + const bRecent = recentIndex.get(b.id); + if (aRecent !== undefined && bRecent !== undefined) return aRecent - bRecent; + if (aRecent !== undefined) return -1; + if (bRecent !== undefined) return 1; + return a.name.localeCompare(b.name); + }); +}