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);
+ });
+}