Default sub-issues to parent workspace

This commit is contained in:
dotta 2026-04-06 11:34:11 -05:00
parent 2775a5652b
commit 758219d53f
4 changed files with 140 additions and 4 deletions

View file

@ -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());
});
});

View file

@ -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"}.
</div>
)}
{showParentWorkspaceWarning ? (
<div className="rounded-md border border-amber-300/60 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-900 dark:border-amber-800/70 dark:bg-amber-950/30 dark:text-amber-100">
Warning: this sub-issue will no longer use the parent issue workspace{parentExecutionWorkspaceLabel ? ` (${parentExecutionWorkspaceLabel})` : ""}.
</div>
) : null}
</div>
</div>
)}

View file

@ -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;

View file

@ -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 }> = [];