diff --git a/ui/src/components/NewIssueDialog.test.tsx b/ui/src/components/NewIssueDialog.test.tsx index 149dd438..d492e907 100644 --- a/ui/src/components/NewIssueDialog.test.tsx +++ b/ui/src/components/NewIssueDialog.test.tsx @@ -311,12 +311,37 @@ describe("NewIssueDialog", () => { }); it("submits parent and goal context for sub-issues", async () => { + mockProjectsApi.list.mockResolvedValue([ + { + id: "project-1", + name: "Alpha", + description: null, + archivedAt: null, + color: "#445566", + executionWorkspacePolicy: { + enabled: true, + defaultMode: "shared_workspace", + }, + }, + ]); + mockExecutionWorkspacesApi.list.mockResolvedValue([ + { + id: "workspace-1", + name: "Parent workspace", + status: "active", + branchName: "feature/pap-1", + cwd: "/tmp/workspace-1", + lastUsedAt: new Date("2026-04-06T16:00:00.000Z"), + }, + ]); + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: true }); dialogState.newIssueDefaults = { parentId: "issue-1", parentIdentifier: "PAP-1", parentTitle: "Parent issue", title: "Child issue", projectId: "project-1", + executionWorkspaceId: "workspace-1", goalId: "goal-1", }; @@ -340,9 +365,75 @@ describe("NewIssueDialog", () => { parentId: "issue-1", goalId: "goal-1", projectId: "project-1", + executionWorkspaceId: "workspace-1", }), ); act(() => root.unmount()); }); + + it("warns when a sub-issue stops matching the parent workspace", async () => { + mockProjectsApi.list.mockResolvedValue([ + { + id: "project-1", + name: "Alpha", + description: null, + archivedAt: null, + color: "#445566", + executionWorkspacePolicy: { + enabled: true, + defaultMode: "shared_workspace", + }, + }, + ]); + mockExecutionWorkspacesApi.list.mockResolvedValue([ + { + id: "workspace-1", + name: "Parent workspace", + status: "active", + branchName: "feature/pap-1", + cwd: "/tmp/workspace-1", + lastUsedAt: new Date("2026-04-06T16:00:00.000Z"), + }, + { + id: "workspace-2", + name: "Other workspace", + status: "active", + branchName: "feature/pap-2", + cwd: "/tmp/workspace-2", + lastUsedAt: new Date("2026-04-06T16:01:00.000Z"), + }, + ]); + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: true }); + dialogState.newIssueDefaults = { + parentId: "issue-1", + parentIdentifier: "PAP-1", + parentTitle: "Parent issue", + title: "Child issue", + projectId: "project-1", + executionWorkspaceId: "workspace-1", + parentExecutionWorkspaceLabel: "Parent workspace", + goalId: "goal-1", + }; + + const { root } = renderDialog(container); + await flush(); + + expect(container.textContent).not.toContain("will no longer use the parent issue workspace"); + + const selects = Array.from(container.querySelectorAll("select")); + const modeSelect = selects[0] as HTMLSelectElement | undefined; + expect(modeSelect).not.toBeUndefined(); + + await act(async () => { + modeSelect!.value = "shared_workspace"; + modeSelect!.dispatchEvent(new Event("change", { bubbles: true })); + }); + await flush(); + + expect(container.textContent).toContain("will no longer use the parent issue workspace"); + expect(container.textContent).toContain("Parent workspace"); + + act(() => root.unmount()); + }); }); diff --git a/ui/src/components/NewIssueDialog.tsx b/ui/src/components/NewIssueDialog.tsx index 1863d494..8e563f49 100644 --- a/ui/src/components/NewIssueDialog.tsx +++ b/ui/src/components/NewIssueDialog.tsx @@ -301,6 +301,8 @@ export function NewIssueDialog() { const isSubIssueMode = Boolean(newIssueDefaults.parentId); const parentIssueLabel = newIssueDefaults.parentIdentifier ?? (newIssueDefaults.parentId ? newIssueDefaults.parentId.slice(0, 8) : ""); + const parentExecutionWorkspaceId = newIssueDefaults.executionWorkspaceId ?? ""; + const parentExecutionWorkspaceLabel = newIssueDefaults.parentExecutionWorkspaceLabel ?? parentExecutionWorkspaceId; // Popover states const [statusOpen, setStatusOpen] = useState(false); @@ -517,18 +519,23 @@ export function NewIssueDialog() { if (newIssueDefaults.parentId) { const defaultProjectId = newIssueDefaults.projectId ?? ""; const defaultProject = orderedProjects.find((project) => project.id === defaultProjectId); + const defaultProjectWorkspaceId = newIssueDefaults.projectWorkspaceId + ?? defaultProjectWorkspaceIdForProject(defaultProject); + const defaultExecutionWorkspaceMode = newIssueDefaults.executionWorkspaceId + ? "reuse_existing" + : (newIssueDefaults.executionWorkspaceMode ?? defaultExecutionWorkspaceModeForProject(defaultProject)); setTitle(newIssueDefaults.title ?? ""); setDescription(newIssueDefaults.description ?? ""); setStatus(newIssueDefaults.status ?? "todo"); setPriority(newIssueDefaults.priority ?? ""); setProjectId(defaultProjectId); - setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(defaultProject)); + setProjectWorkspaceId(defaultProjectWorkspaceId); setAssigneeValue(assigneeValueFromSelection(newIssueDefaults)); setAssigneeModelOverride(""); setAssigneeThinkingEffort(""); setAssigneeChrome(false); - setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(defaultProject)); - setSelectedExecutionWorkspaceId(""); + setExecutionWorkspaceMode(defaultExecutionWorkspaceMode); + setSelectedExecutionWorkspaceId(newIssueDefaults.executionWorkspaceId ?? ""); executionWorkspaceDefaultProjectId.current = defaultProjectId || null; } else if (newIssueDefaults.title) { setTitle(newIssueDefaults.title); @@ -797,6 +804,13 @@ export function NewIssueDialog() { const selectedReusableExecutionWorkspace = deduplicatedReusableWorkspaces.find( (workspace) => workspace.id === selectedExecutionWorkspaceId, ); + const isUsingParentExecutionWorkspace = isSubIssueMode && parentExecutionWorkspaceId + ? executionWorkspaceMode === "reuse_existing" && selectedExecutionWorkspaceId === parentExecutionWorkspaceId + : false; + const showParentWorkspaceWarning = isSubIssueMode + && currentProjectSupportsExecutionWorkspace + && Boolean(parentExecutionWorkspaceId) + && !isUsingParentExecutionWorkspace; const assigneeOptionsTitle = assigneeAdapterType === "claude_local" ? "Claude options" @@ -1202,6 +1216,11 @@ export function NewIssueDialog() { Reusing {selectedReusableExecutionWorkspace.name} from {selectedReusableExecutionWorkspace.branchName ?? selectedReusableExecutionWorkspace.cwd ?? "existing execution workspace"}. )} + {showParentWorkspaceWarning ? ( +
+ Warning: this sub-issue will no longer use the parent issue workspace{parentExecutionWorkspaceLabel ? ` (${parentExecutionWorkspaceLabel})` : ""}. +
+ ) : null} )} diff --git a/ui/src/context/DialogContext.tsx b/ui/src/context/DialogContext.tsx index bd475b33..bf18c7db 100644 --- a/ui/src/context/DialogContext.tsx +++ b/ui/src/context/DialogContext.tsx @@ -4,10 +4,14 @@ interface NewIssueDefaults { status?: string; priority?: string; projectId?: string; + projectWorkspaceId?: string; goalId?: string; parentId?: string; parentIdentifier?: string; parentTitle?: string; + executionWorkspaceId?: string; + executionWorkspaceMode?: string; + parentExecutionWorkspaceLabel?: string; assigneeAgentId?: string; assigneeUserId?: string; title?: string; diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 84e27700..7c80eb71 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -507,9 +507,31 @@ export function IssueDetail() { 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, }); - }, [issue?.goalId, issue?.id, issue?.identifier, issue?.projectId, issue?.title, openNewIssue]); + }, [ + 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, + openNewIssue, + ]); const commentReassignOptions = useMemo(() => { const options: Array<{ id: string; label: string; searchText?: string }> = [];