diff --git a/ui/src/components/IssuesList.tsx b/ui/src/components/IssuesList.tsx index 9dde9e08807..062514b9be1 100644 --- a/ui/src/components/IssuesList.tsx +++ b/ui/src/components/IssuesList.tsx @@ -53,6 +53,7 @@ import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/component import { CircleDot, Plus, ArrowUpDown, Layers, Check, ChevronRight, List, Columns3, User, Search } from "lucide-react"; import { KanbanBoard } from "./KanbanBoard"; import { buildIssueTree, countDescendants } from "../lib/issue-tree"; +import { buildSubIssueDefaults } from "../lib/subIssueDefaults"; import type { Issue, Project } from "@paperclipai/shared"; const ISSUE_SEARCH_DEBOUNCE_MS = 150; @@ -494,11 +495,13 @@ export function IssuesList({ else defaults.assigneeAgentId = groupKey; } else if (viewState.groupBy === "parent" && groupKey !== "__no_parent") { - defaults.parentId = groupKey; + const parentIssue = issueById.get(groupKey); + if (parentIssue) Object.assign(defaults, buildSubIssueDefaults(parentIssue)); + else defaults.parentId = groupKey; } } return defaults; - }, [projectId, viewState.groupBy]); + }, [issueById, projectId, viewState.groupBy]); const filterToWorkspace = useCallback((workspaceId: string) => { updateView({ workspaces: [workspaceId] }); diff --git a/ui/src/components/NewIssueDialog.test.tsx b/ui/src/components/NewIssueDialog.test.tsx index b81b30e4327..4f35be6519b 100644 --- a/ui/src/components/NewIssueDialog.test.tsx +++ b/ui/src/components/NewIssueDialog.test.tsx @@ -372,6 +372,43 @@ describe("NewIssueDialog", () => { act(() => root.unmount()); }); + it("submits the parent assignee when a sub-issue opens with inherited defaults", async () => { + dialogState.newIssueDefaults = { + parentId: "issue-1", + parentIdentifier: "PAP-1", + parentTitle: "Parent issue", + title: "Child issue", + projectId: "project-1", + goalId: "goal-1", + assigneeAgentId: "agent-1", + }; + + const { root } = renderDialog(container); + await flush(); + + const submitButton = Array.from(container.querySelectorAll("button")) + .find((button) => button.textContent?.includes("Create Sub-Issue")); + expect(submitButton).not.toBeUndefined(); + + await act(async () => { + submitButton!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flush(); + + expect(mockIssuesApi.create).toHaveBeenCalledWith( + "company-1", + expect.objectContaining({ + title: "Child issue", + parentId: "issue-1", + goalId: "goal-1", + projectId: "project-1", + assigneeAgentId: "agent-1", + }), + ); + + act(() => root.unmount()); + }); + it("keeps the mobile dialog bounded with an internal flexible scroll region", async () => { const { root } = renderDialog(container); await flush(); diff --git a/ui/src/lib/subIssueDefaults.test.ts b/ui/src/lib/subIssueDefaults.test.ts new file mode 100644 index 00000000000..ca34d654d8b --- /dev/null +++ b/ui/src/lib/subIssueDefaults.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from "vitest"; +import type { ExecutionWorkspace, Issue } from "@paperclipai/shared"; +import { buildSubIssueDefaults } from "./subIssueDefaults"; + +function makeExecutionWorkspace(overrides: Partial = {}): ExecutionWorkspace { + return { + id: "workspace-1", + companyId: "company-1", + projectId: "project-1", + projectWorkspaceId: "project-workspace-1", + sourceIssueId: null, + status: "active", + mode: "isolated_workspace", + strategyType: "git_worktree", + name: "Parent workspace", + cwd: "/tmp/workspace-1", + repoUrl: null, + baseRef: null, + branchName: "feature/pap-1", + providerType: "git_worktree", + providerRef: null, + derivedFromExecutionWorkspaceId: null, + openedAt: new Date("2026-04-07T00:00:00.000Z"), + closedAt: null, + cleanupEligibleAt: null, + cleanupReason: null, + config: null, + metadata: null, + lastUsedAt: new Date("2026-04-07T00:00:00.000Z"), + createdAt: new Date("2026-04-07T00:00:00.000Z"), + updatedAt: new Date("2026-04-07T00:00:00.000Z"), + ...overrides, + }; +} + +function makeIssue(overrides: Partial = {}): Issue { + return { + id: "issue-1", + identifier: "PAP-1", + companyId: "company-1", + projectId: "project-1", + projectWorkspaceId: "project-workspace-1", + goalId: "goal-1", + parentId: null, + title: "Parent issue", + description: null, + status: "todo", + priority: "medium", + assigneeAgentId: null, + assigneeUserId: null, + checkoutRunId: null, + executionRunId: null, + executionAgentNameKey: null, + executionLockedAt: null, + createdByAgentId: null, + createdByUserId: null, + issueNumber: 1, + requestDepth: 0, + billingCode: null, + assigneeAdapterOverrides: null, + executionWorkspaceId: null, + executionWorkspacePreference: "shared_workspace", + executionWorkspaceSettings: null, + currentExecutionWorkspace: null, + startedAt: null, + completedAt: null, + cancelledAt: null, + hiddenAt: null, + createdAt: new Date("2026-04-07T00:00:00.000Z"), + updatedAt: new Date("2026-04-07T00:00:00.000Z"), + ...overrides, + }; +} + +describe("buildSubIssueDefaults", () => { + it("inherits the parent agent assignee and workspace context", () => { + const defaults = buildSubIssueDefaults( + makeIssue({ + assigneeAgentId: "agent-1", + executionWorkspaceId: "workspace-1", + currentExecutionWorkspace: makeExecutionWorkspace(), + }), + ); + + expect(defaults).toEqual({ + parentId: "issue-1", + parentIdentifier: "PAP-1", + parentTitle: "Parent issue", + projectId: "project-1", + projectWorkspaceId: "project-workspace-1", + goalId: "goal-1", + executionWorkspaceId: "workspace-1", + executionWorkspaceMode: "reuse_existing", + parentExecutionWorkspaceLabel: "Parent workspace", + assigneeAgentId: "agent-1", + }); + }); + + it("inherits a user assignee when the parent is assigned to a user", () => { + const defaults = buildSubIssueDefaults( + makeIssue({ + assigneeUserId: "user-1", + }), + ); + + expect(defaults).toEqual({ + parentId: "issue-1", + parentIdentifier: "PAP-1", + parentTitle: "Parent issue", + projectId: "project-1", + projectWorkspaceId: "project-workspace-1", + goalId: "goal-1", + executionWorkspaceMode: "shared_workspace", + assigneeUserId: "user-1", + }); + }); +}); diff --git a/ui/src/lib/subIssueDefaults.ts b/ui/src/lib/subIssueDefaults.ts new file mode 100644 index 00000000000..e26ae1a17d4 --- /dev/null +++ b/ui/src/lib/subIssueDefaults.ts @@ -0,0 +1,43 @@ +import type { Issue } from "@paperclipai/shared"; + +type SubIssueDefaultSource = Pick< + Issue, + | "id" + | "identifier" + | "title" + | "projectId" + | "projectWorkspaceId" + | "goalId" + | "executionWorkspaceId" + | "executionWorkspacePreference" + | "currentExecutionWorkspace" + | "assigneeAgentId" + | "assigneeUserId" +>; + +export function buildSubIssueDefaults(issue: SubIssueDefaultSource) { + const parentExecutionWorkspaceLabel = + issue.currentExecutionWorkspace?.name + ?? issue.currentExecutionWorkspace?.branchName + ?? issue.currentExecutionWorkspace?.cwd + ?? issue.executionWorkspaceId + ?? null; + + return { + parentId: issue.id, + parentIdentifier: issue.identifier ?? undefined, + parentTitle: issue.title, + ...(issue.projectId ? { projectId: issue.projectId } : {}), + ...(issue.projectWorkspaceId ? { projectWorkspaceId: issue.projectWorkspaceId } : {}), + ...(issue.goalId ? { goalId: issue.goalId } : {}), + ...(issue.executionWorkspaceId ? { executionWorkspaceId: issue.executionWorkspaceId } : {}), + ...(issue.executionWorkspaceId + ? { executionWorkspaceMode: "reuse_existing" } + : issue.executionWorkspacePreference + ? { executionWorkspaceMode: issue.executionWorkspacePreference } + : {}), + ...(parentExecutionWorkspaceLabel ? { parentExecutionWorkspaceLabel } : {}), + ...(issue.assigneeAgentId ? { assigneeAgentId: issue.assigneeAgentId } : {}), + ...(issue.assigneeUserId ? { assigneeUserId: issue.assigneeUserId } : {}), + }; +} diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 33b55060a74..55dfad7bf2c 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -78,6 +78,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { formatIssueActivityAction } from "@/lib/activity-format"; import { buildIssuePropertiesPanelKey } from "../lib/issue-properties-panel-key"; import { resolveIssueChatTranscriptRuns } from "../lib/issueChatTranscriptRuns"; +import { buildSubIssueDefaults } from "../lib/subIssueDefaults"; import { Activity as ActivityIcon, Archive, @@ -749,34 +750,9 @@ export function IssueDetail() { ); const openNewSubIssue = useCallback(() => { if (!issue) return; - openNewIssue({ - parentId: issue.id, - parentIdentifier: issue.identifier ?? undefined, - parentTitle: issue.title, - projectId: issue.projectId ?? undefined, - projectWorkspaceId: issue.projectWorkspaceId ?? undefined, - goalId: issue.goalId ?? undefined, - executionWorkspaceId: issue.executionWorkspaceId ?? undefined, - executionWorkspaceMode: issue.executionWorkspaceId ? "reuse_existing" : issue.executionWorkspacePreference ?? undefined, - parentExecutionWorkspaceLabel: - issue.currentExecutionWorkspace?.name - ?? issue.currentExecutionWorkspace?.branchName - ?? issue.currentExecutionWorkspace?.cwd - ?? issue.executionWorkspaceId - ?? undefined, - }); + openNewIssue(buildSubIssueDefaults(issue)); }, [ - issue?.currentExecutionWorkspace?.branchName, - issue?.currentExecutionWorkspace?.cwd, - issue?.currentExecutionWorkspace?.name, - issue?.executionWorkspaceId, - issue?.executionWorkspacePreference, - issue?.goalId, - issue?.id, - issue?.identifier, - issue?.projectId, - issue?.projectWorkspaceId, - issue?.title, + issue, openNewIssue, ]);