-
Assignee
-
- {agents.map((agent) => (
-
- ))}
-
+
+
Assignee
+
+
+ {currentUserId && (
+
+ )}
+ {(agents ?? []).map((agent) => (
+
+ ))}
- )}
+
{labels && labels.length > 0 && (
@@ -683,6 +725,13 @@ export function IssuesList({
>
{issue.assigneeAgentId && agentName(issue.assigneeAgentId) ? (
+ ) : issue.assigneeUserId ? (
+
+
+
+
+ {formatAssigneeUserLabel(issue.assigneeUserId, currentUserId) ?? "User"}
+
) : (
@@ -701,7 +750,7 @@ export function IssuesList({
>
setAssigneeSearch(e.target.value)}
autoFocus
@@ -710,16 +759,32 @@ export function IssuesList({
+ {currentUserId && (
+
+ )}
{(agents ?? [])
.filter((agent) => {
if (!assigneeSearch.trim()) return true;
@@ -737,7 +802,7 @@ export function IssuesList({
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
- assignIssue(issue.id, agent.id);
+ assignIssue(issue.id, agent.id, null);
}}
>
diff --git a/ui/src/context/DialogContext.tsx b/ui/src/context/DialogContext.tsx
index ef7b12b8..904ceb88 100644
--- a/ui/src/context/DialogContext.tsx
+++ b/ui/src/context/DialogContext.tsx
@@ -5,6 +5,7 @@ interface NewIssueDefaults {
priority?: string;
projectId?: string;
assigneeAgentId?: string;
+ assigneeUserId?: string;
title?: string;
description?: string;
}
diff --git a/ui/src/lib/assignees.test.ts b/ui/src/lib/assignees.test.ts
new file mode 100644
index 00000000..1ce22ef7
--- /dev/null
+++ b/ui/src/lib/assignees.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from "vitest";
+import {
+ assigneeValueFromSelection,
+ currentUserAssigneeOption,
+ formatAssigneeUserLabel,
+ parseAssigneeValue,
+} from "./assignees";
+
+describe("assignee selection helpers", () => {
+ it("encodes and parses agent assignees", () => {
+ const value = assigneeValueFromSelection({ assigneeAgentId: "agent-123" });
+
+ expect(value).toBe("agent:agent-123");
+ expect(parseAssigneeValue(value)).toEqual({
+ assigneeAgentId: "agent-123",
+ assigneeUserId: null,
+ });
+ });
+
+ it("encodes and parses current-user assignees", () => {
+ const [option] = currentUserAssigneeOption("local-board");
+
+ expect(option).toEqual({
+ id: "user:local-board",
+ label: "Me",
+ searchText: "me board human local-board",
+ });
+ expect(parseAssigneeValue(option.id)).toEqual({
+ assigneeAgentId: null,
+ assigneeUserId: "local-board",
+ });
+ });
+
+ it("treats an empty selection as no assignee", () => {
+ expect(parseAssigneeValue("")).toEqual({
+ assigneeAgentId: null,
+ assigneeUserId: null,
+ });
+ });
+
+ it("keeps backward compatibility for raw agent ids in saved drafts", () => {
+ expect(parseAssigneeValue("legacy-agent-id")).toEqual({
+ assigneeAgentId: "legacy-agent-id",
+ assigneeUserId: null,
+ });
+ });
+
+ it("formats current and board user labels consistently", () => {
+ expect(formatAssigneeUserLabel("user-1", "user-1")).toBe("Me");
+ expect(formatAssigneeUserLabel("local-board", "someone-else")).toBe("Board");
+ expect(formatAssigneeUserLabel("user-abcdef", "someone-else")).toBe("user-");
+ });
+});
diff --git a/ui/src/lib/assignees.ts b/ui/src/lib/assignees.ts
new file mode 100644
index 00000000..274bcd40
--- /dev/null
+++ b/ui/src/lib/assignees.ts
@@ -0,0 +1,51 @@
+export interface AssigneeSelection {
+ assigneeAgentId: string | null;
+ assigneeUserId: string | null;
+}
+
+export interface AssigneeOption {
+ id: string;
+ label: string;
+ searchText?: string;
+}
+
+export function assigneeValueFromSelection(selection: Partial): string {
+ if (selection.assigneeAgentId) return `agent:${selection.assigneeAgentId}`;
+ if (selection.assigneeUserId) return `user:${selection.assigneeUserId}`;
+ return "";
+}
+
+export function parseAssigneeValue(value: string): AssigneeSelection {
+ if (!value) {
+ return { assigneeAgentId: null, assigneeUserId: null };
+ }
+ if (value.startsWith("agent:")) {
+ const assigneeAgentId = value.slice("agent:".length);
+ return { assigneeAgentId: assigneeAgentId || null, assigneeUserId: null };
+ }
+ if (value.startsWith("user:")) {
+ const assigneeUserId = value.slice("user:".length);
+ return { assigneeAgentId: null, assigneeUserId: assigneeUserId || null };
+ }
+ // Backward compatibility for older drafts/defaults that stored a raw agent id.
+ return { assigneeAgentId: value, assigneeUserId: null };
+}
+
+export function currentUserAssigneeOption(currentUserId: string | null | undefined): AssigneeOption[] {
+ if (!currentUserId) return [];
+ return [{
+ id: assigneeValueFromSelection({ assigneeUserId: currentUserId }),
+ label: "Me",
+ searchText: currentUserId === "local-board" ? "me board human local-board" : `me human ${currentUserId}`,
+ }];
+}
+
+export function formatAssigneeUserLabel(
+ userId: string | null | undefined,
+ currentUserId: string | null | undefined,
+): string | null {
+ if (!userId) return null;
+ if (currentUserId && userId === currentUserId) return "Me";
+ if (userId === "local-board") return "Board";
+ return userId.slice(0, 5);
+}
diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx
index bb152e17..9a43f26a 100644
--- a/ui/src/pages/IssueDetail.tsx
+++ b/ui/src/pages/IssueDetail.tsx
@@ -304,8 +304,7 @@ export function IssueDetail() {
options.push({ id: `agent:${agent.id}`, label: agent.name });
}
if (currentUserId) {
- const label = currentUserId === "local-board" ? "Board" : "Me (Board)";
- options.push({ id: `user:${currentUserId}`, label });
+ options.push({ id: `user:${currentUserId}`, label: "Me" });
}
return options;
}, [agents, currentUserId]);