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 ? ( +