Add project-first execution workspace policies

This commit is contained in:
Dotta
2026-03-10 09:03:31 -05:00
parent 3120c72372
commit b83a87f42f
22 changed files with 7046 additions and 114 deletions

View File

@@ -1,5 +1,6 @@
import { useState } from "react";
import type { AdapterConfigFieldsProps } from "./types";
import { DraftInput, Field, help } from "../components/agent-config-primitives";
import { CollapsibleSection, DraftInput, Field, help } from "../components/agent-config-primitives";
import { RuntimeServicesJsonField } from "./runtime-json-fields";
const inputClass =
@@ -48,6 +49,7 @@ export function LocalWorkspaceRuntimeFields({
config,
mark,
}: AdapterConfigFieldsProps) {
const [open, setOpen] = useState(false);
const existing = readWorkspaceStrategy(config);
const strategyType = isCreate ? values!.workspaceStrategyType ?? "project_primary" : existing.type;
const updateEditWorkspaceStrategy = (patch: Partial<typeof existing>) => {
@@ -62,75 +64,81 @@ export function LocalWorkspaceRuntimeFields({
);
};
return (
<>
<Field label="Workspace strategy" hint={help.workspaceStrategy}>
<select
className={inputClass}
value={strategyType}
onChange={(e) => {
const nextType = e.target.value;
if (isCreate) {
set!({ workspaceStrategyType: nextType });
} else {
updateEditWorkspaceStrategy({ type: nextType });
}
}}
>
<option value="project_primary">Project primary workspace</option>
<option value="git_worktree">Git worktree</option>
</select>
</Field>
<CollapsibleSection
title="Advanced Workspace Overrides"
open={open}
onToggle={() => setOpen((value) => !value)}
>
<div className="space-y-3 pt-1">
<Field label="Workspace strategy" hint={help.workspaceStrategy}>
<select
className={inputClass}
value={strategyType}
onChange={(e) => {
const nextType = e.target.value;
if (isCreate) {
set!({ workspaceStrategyType: nextType });
} else {
updateEditWorkspaceStrategy({ type: nextType });
}
}}
>
<option value="project_primary">Project primary workspace</option>
<option value="git_worktree">Git worktree</option>
</select>
</Field>
{strategyType === "git_worktree" && (
<>
<Field label="Base ref" hint={help.workspaceBaseRef}>
<DraftInput
value={isCreate ? values!.workspaceBaseRef ?? "" : existing.baseRef}
onCommit={(v) =>
isCreate
? set!({ workspaceBaseRef: v })
: updateEditWorkspaceStrategy({ baseRef: v || "" })
}
immediate
className={inputClass}
placeholder="origin/main"
/>
</Field>
<Field label="Branch template" hint={help.workspaceBranchTemplate}>
<DraftInput
value={isCreate ? values!.workspaceBranchTemplate ?? "" : existing.branchTemplate}
onCommit={(v) =>
isCreate
? set!({ workspaceBranchTemplate: v })
: updateEditWorkspaceStrategy({ branchTemplate: v || "" })
}
immediate
className={inputClass}
placeholder="{{issue.identifier}}-{{slug}}"
/>
</Field>
<Field label="Worktree parent dir" hint={help.worktreeParentDir}>
<DraftInput
value={isCreate ? values!.worktreeParentDir ?? "" : existing.worktreeParentDir}
onCommit={(v) =>
isCreate
? set!({ worktreeParentDir: v })
: updateEditWorkspaceStrategy({ worktreeParentDir: v || "" })
}
immediate
className={inputClass}
placeholder=".paperclip/worktrees"
/>
</Field>
</>
)}
<RuntimeServicesJsonField
isCreate={isCreate}
values={values}
set={set}
config={config}
mark={mark}
/>
</>
{strategyType === "git_worktree" && (
<>
<Field label="Base ref" hint={help.workspaceBaseRef}>
<DraftInput
value={isCreate ? values!.workspaceBaseRef ?? "" : existing.baseRef}
onCommit={(v) =>
isCreate
? set!({ workspaceBaseRef: v })
: updateEditWorkspaceStrategy({ baseRef: v || "" })
}
immediate
className={inputClass}
placeholder="origin/main"
/>
</Field>
<Field label="Branch template" hint={help.workspaceBranchTemplate}>
<DraftInput
value={isCreate ? values!.workspaceBranchTemplate ?? "" : existing.branchTemplate}
onCommit={(v) =>
isCreate
? set!({ workspaceBranchTemplate: v })
: updateEditWorkspaceStrategy({ branchTemplate: v || "" })
}
immediate
className={inputClass}
placeholder="{{issue.identifier}}-{{slug}}"
/>
</Field>
<Field label="Worktree parent dir" hint={help.worktreeParentDir}>
<DraftInput
value={isCreate ? values!.worktreeParentDir ?? "" : existing.worktreeParentDir}
onCommit={(v) =>
isCreate
? set!({ worktreeParentDir: v })
: updateEditWorkspaceStrategy({ worktreeParentDir: v || "" })
}
immediate
className={inputClass}
placeholder=".paperclip/worktrees"
/>
</Field>
</>
)}
<RuntimeServicesJsonField
isCreate={isCreate}
values={values}
set={set}
config={config}
mark={mark}
/>
</div>
</CollapsibleSection>
);
}

View File

@@ -176,6 +176,16 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
const project = orderedProjects.find((p) => p.id === id);
return project?.name ?? id.slice(0, 8);
};
const currentProject = issue.projectId
? orderedProjects.find((project) => project.id === issue.projectId) ?? null
: null;
const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null;
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
const usesIsolatedExecutionWorkspace = issue.executionWorkspaceSettings?.mode === "isolated"
? true
: issue.executionWorkspaceSettings?.mode === "project_primary"
? false
: currentProjectExecutionWorkspacePolicy?.defaultMode === "isolated";
const projectLink = (id: string | null) => {
if (!id) return null;
const project = projects?.find((p) => p.id === id) ?? null;
@@ -402,7 +412,10 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 whitespace-nowrap",
!issue.projectId && "bg-accent"
)}
onClick={() => { onUpdate({ projectId: null }); setProjectOpen(false); }}
onClick={() => {
onUpdate({ projectId: null, executionWorkspaceSettings: null });
setProjectOpen(false);
}}
>
No project
</button>
@@ -419,7 +432,15 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 whitespace-nowrap",
p.id === issue.projectId && "bg-accent"
)}
onClick={() => { onUpdate({ projectId: p.id }); setProjectOpen(false); }}
onClick={() => {
onUpdate({
projectId: p.id,
executionWorkspaceSettings: p.executionWorkspacePolicy?.enabled
? { mode: p.executionWorkspacePolicy.defaultMode === "isolated" ? "isolated" : "project_primary" }
: null,
});
setProjectOpen(false);
}}
>
<span
className="shrink-0 h-3 w-3 rounded-sm"
@@ -504,6 +525,42 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
{projectContent}
</PropertyPicker>
{currentProjectSupportsExecutionWorkspace && (
<PropertyRow label="Workspace">
<div className="flex items-center justify-between gap-3 rounded-md border border-border px-2 py-1.5 w-full">
<div className="min-w-0">
<div className="text-sm">
{usesIsolatedExecutionWorkspace ? "Isolated issue checkout" : "Project primary checkout"}
</div>
<div className="text-[11px] text-muted-foreground">
Toggle whether this issue runs in its own execution workspace.
</div>
</div>
<button
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
usesIsolatedExecutionWorkspace ? "bg-green-600" : "bg-muted",
)}
type="button"
onClick={() =>
onUpdate({
executionWorkspaceSettings: {
mode: usesIsolatedExecutionWorkspace ? "project_primary" : "isolated",
},
})
}
>
<span
className={cn(
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
usesIsolatedExecutionWorkspace ? "translate-x-4.5" : "translate-x-0.5",
)}
/>
</button>
</div>
</PropertyRow>
)}
{issue.parentId && (
<PropertyRow label="Parent">
<Link

View File

@@ -65,7 +65,7 @@ interface IssueDraft {
assigneeModelOverride: string;
assigneeThinkingEffort: string;
assigneeChrome: boolean;
assigneeUseProjectWorkspace: boolean;
useIsolatedExecutionWorkspace: boolean;
}
const ISSUE_OVERRIDE_ADAPTER_TYPES = new Set(["claude_local", "codex_local", "opencode_local"]);
@@ -99,7 +99,6 @@ function buildAssigneeAdapterOverrides(input: {
modelOverride: string;
thinkingEffortOverride: string;
chrome: boolean;
useProjectWorkspace: boolean;
}): Record<string, unknown> | null {
const adapterType = input.adapterType ?? null;
if (!adapterType || !ISSUE_OVERRIDE_ADAPTER_TYPES.has(adapterType)) {
@@ -127,9 +126,6 @@ function buildAssigneeAdapterOverrides(input: {
if (Object.keys(adapterConfig).length > 0) {
overrides.adapterConfig = adapterConfig;
}
if (!input.useProjectWorkspace) {
overrides.useProjectWorkspace = false;
}
return Object.keys(overrides).length > 0 ? overrides : null;
}
@@ -180,10 +176,11 @@ export function NewIssueDialog() {
const [assigneeModelOverride, setAssigneeModelOverride] = useState("");
const [assigneeThinkingEffort, setAssigneeThinkingEffort] = useState("");
const [assigneeChrome, setAssigneeChrome] = useState(false);
const [assigneeUseProjectWorkspace, setAssigneeUseProjectWorkspace] = useState(true);
const [useIsolatedExecutionWorkspace, setUseIsolatedExecutionWorkspace] = useState(false);
const [expanded, setExpanded] = useState(false);
const [dialogCompanyId, setDialogCompanyId] = useState<string | null>(null);
const draftTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const executionWorkspaceDefaultProjectId = useRef<string | null>(null);
const effectiveCompanyId = dialogCompanyId ?? selectedCompanyId;
const dialogCompany = companies.find((c) => c.id === effectiveCompanyId) ?? selectedCompany;
@@ -300,7 +297,7 @@ export function NewIssueDialog() {
assigneeModelOverride,
assigneeThinkingEffort,
assigneeChrome,
assigneeUseProjectWorkspace,
useIsolatedExecutionWorkspace,
});
}, [
title,
@@ -312,7 +309,7 @@ export function NewIssueDialog() {
assigneeModelOverride,
assigneeThinkingEffort,
assigneeChrome,
assigneeUseProjectWorkspace,
useIsolatedExecutionWorkspace,
newIssueOpen,
scheduleSave,
]);
@@ -321,6 +318,7 @@ export function NewIssueDialog() {
useEffect(() => {
if (!newIssueOpen) return;
setDialogCompanyId(selectedCompanyId);
executionWorkspaceDefaultProjectId.current = null;
const draft = loadDraft();
if (newIssueDefaults.title) {
@@ -333,7 +331,7 @@ export function NewIssueDialog() {
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
setAssigneeChrome(false);
setAssigneeUseProjectWorkspace(true);
setUseIsolatedExecutionWorkspace(false);
} else if (draft && draft.title.trim()) {
setTitle(draft.title);
setDescription(draft.description);
@@ -344,7 +342,7 @@ export function NewIssueDialog() {
setAssigneeModelOverride(draft.assigneeModelOverride ?? "");
setAssigneeThinkingEffort(draft.assigneeThinkingEffort ?? "");
setAssigneeChrome(draft.assigneeChrome ?? false);
setAssigneeUseProjectWorkspace(draft.assigneeUseProjectWorkspace ?? true);
setUseIsolatedExecutionWorkspace(draft.useIsolatedExecutionWorkspace ?? false);
} else {
setStatus(newIssueDefaults.status ?? "todo");
setPriority(newIssueDefaults.priority ?? "");
@@ -353,7 +351,7 @@ export function NewIssueDialog() {
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
setAssigneeChrome(false);
setAssigneeUseProjectWorkspace(true);
setUseIsolatedExecutionWorkspace(false);
}
}, [newIssueOpen, newIssueDefaults]);
@@ -363,7 +361,6 @@ export function NewIssueDialog() {
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
setAssigneeChrome(false);
setAssigneeUseProjectWorkspace(true);
return;
}
@@ -396,10 +393,11 @@ export function NewIssueDialog() {
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
setAssigneeChrome(false);
setAssigneeUseProjectWorkspace(true);
setUseIsolatedExecutionWorkspace(false);
setExpanded(false);
setDialogCompanyId(null);
setCompanyOpen(false);
executionWorkspaceDefaultProjectId.current = null;
}
function handleCompanyChange(companyId: string) {
@@ -410,7 +408,7 @@ export function NewIssueDialog() {
setAssigneeModelOverride("");
setAssigneeThinkingEffort("");
setAssigneeChrome(false);
setAssigneeUseProjectWorkspace(true);
setUseIsolatedExecutionWorkspace(false);
}
function discardDraft() {
@@ -426,8 +424,14 @@ export function NewIssueDialog() {
modelOverride: assigneeModelOverride,
thinkingEffortOverride: assigneeThinkingEffort,
chrome: assigneeChrome,
useProjectWorkspace: assigneeUseProjectWorkspace,
});
const selectedProject = orderedProjects.find((project) => project.id === projectId);
const executionWorkspacePolicy = selectedProject?.executionWorkspacePolicy;
const executionWorkspaceSettings = executionWorkspacePolicy?.enabled
? {
mode: useIsolatedExecutionWorkspace ? "isolated" : "project_primary",
}
: null;
createIssue.mutate({
companyId: effectiveCompanyId,
title: title.trim(),
@@ -437,6 +441,7 @@ export function NewIssueDialog() {
...(assigneeId ? { assigneeAgentId: assigneeId } : {}),
...(projectId ? { projectId } : {}),
...(assigneeAdapterOverrides ? { assigneeAdapterOverrides } : {}),
...(executionWorkspaceSettings ? { executionWorkspaceSettings } : {}),
});
}
@@ -467,6 +472,8 @@ export function NewIssueDialog() {
const currentPriority = priorities.find((p) => p.value === priority);
const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId);
const currentProject = orderedProjects.find((project) => project.id === projectId);
const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null;
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
const assigneeOptionsTitle =
assigneeAdapterType === "claude_local"
? "Claude options"
@@ -503,6 +510,26 @@ export function NewIssueDialog() {
})),
[orderedProjects],
);
const handleProjectChange = useCallback((nextProjectId: string) => {
setProjectId(nextProjectId);
const nextProject = orderedProjects.find((project) => project.id === nextProjectId);
const policy = nextProject?.executionWorkspacePolicy;
executionWorkspaceDefaultProjectId.current = nextProjectId || null;
setUseIsolatedExecutionWorkspace(Boolean(policy?.enabled && policy.defaultMode === "isolated"));
}, [orderedProjects]);
useEffect(() => {
if (!newIssueOpen || !projectId || executionWorkspaceDefaultProjectId.current === projectId) {
return;
}
const project = orderedProjects.find((entry) => entry.id === projectId);
if (!project) return;
executionWorkspaceDefaultProjectId.current = projectId;
setUseIsolatedExecutionWorkspace(
Boolean(project.executionWorkspacePolicy?.enabled && project.executionWorkspacePolicy.defaultMode === "isolated"),
);
}, [newIssueOpen, orderedProjects, projectId]);
const modelOverrideOptions = useMemo<InlineEntityOption[]>(
() => {
return [...(assigneeAdapterModels ?? [])]
@@ -705,7 +732,7 @@ export function NewIssueDialog() {
noneLabel="No project"
searchPlaceholder="Search projects..."
emptyMessage="No projects found."
onChange={setProjectId}
onChange={handleProjectChange}
onConfirm={() => {
descriptionEditorRef.current?.focus();
}}
@@ -740,6 +767,34 @@ export function NewIssueDialog() {
</div>
</div>
{currentProjectSupportsExecutionWorkspace && (
<div className="px-4 pb-2 shrink-0">
<div className="flex items-center justify-between rounded-md border border-border px-3 py-2">
<div className="space-y-0.5">
<div className="text-xs font-medium">Use isolated issue checkout</div>
<div className="text-[11px] text-muted-foreground">
Create an issue-specific execution workspace instead of using the project's primary checkout.
</div>
</div>
<button
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
useIsolatedExecutionWorkspace ? "bg-green-600" : "bg-muted",
)}
onClick={() => setUseIsolatedExecutionWorkspace((value) => !value)}
type="button"
>
<span
className={cn(
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
useIsolatedExecutionWorkspace ? "translate-x-4.5" : "translate-x-0.5",
)}
/>
</button>
</div>
</div>
)}
{supportsAssigneeOverrides && (
<div className="px-4 pb-2 shrink-0">
<button
@@ -800,23 +855,6 @@ export function NewIssueDialog() {
</button>
</div>
)}
<div className="flex items-center justify-between rounded-md border border-border px-2 py-1.5">
<div className="text-xs text-muted-foreground">Use project workspace</div>
<button
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
assigneeUseProjectWorkspace ? "bg-green-600" : "bg-muted"
)}
onClick={() => setAssigneeUseProjectWorkspace((value) => !value)}
>
<span
className={cn(
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
assigneeUseProjectWorkspace ? "translate-x-4.5" : "translate-x-0.5"
)}
/>
</button>
</div>
</div>
)}
</div>

View File

@@ -15,6 +15,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { ExternalLink, Github, Plus, Trash2, X } from "lucide-react";
import { ChoosePathButton } from "./PathInstructionsModal";
import { CollapsibleSection, DraftInput } from "./agent-config-primitives";
const PROJECT_STATUSES = [
{ value: "backlog", label: "Backlog" },
@@ -80,6 +81,7 @@ export function ProjectProperties({ project, onUpdate }: ProjectPropertiesProps)
const { selectedCompanyId } = useCompany();
const queryClient = useQueryClient();
const [goalOpen, setGoalOpen] = useState(false);
const [executionWorkspaceAdvancedOpen, setExecutionWorkspaceAdvancedOpen] = useState(false);
const [workspaceMode, setWorkspaceMode] = useState<"local" | "repo" | null>(null);
const [workspaceCwd, setWorkspaceCwd] = useState("");
const [workspaceRepoUrl, setWorkspaceRepoUrl] = useState("");
@@ -106,6 +108,16 @@ export function ProjectProperties({ project, onUpdate }: ProjectPropertiesProps)
const availableGoals = (allGoals ?? []).filter((g) => !linkedGoalIds.includes(g.id));
const workspaces = project.workspaces ?? [];
const executionWorkspacePolicy = project.executionWorkspacePolicy ?? null;
const executionWorkspacesEnabled = executionWorkspacePolicy?.enabled === true;
const executionWorkspaceDefaultMode =
executionWorkspacePolicy?.defaultMode === "isolated" ? "isolated" : "project_primary";
const executionWorkspaceStrategy = executionWorkspacePolicy?.workspaceStrategy ?? {
type: "git_worktree",
baseRef: "",
branchTemplate: "",
worktreeParentDir: "",
};
const invalidateProject = () => {
queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(project.id) });
@@ -146,6 +158,19 @@ export function ProjectProperties({ project, onUpdate }: ProjectPropertiesProps)
setGoalOpen(false);
};
const updateExecutionWorkspacePolicy = (patch: Record<string, unknown>) => {
if (!onUpdate) return;
onUpdate({
executionWorkspacePolicy: {
enabled: executionWorkspacesEnabled,
defaultMode: executionWorkspaceDefaultMode,
allowIssueOverride: executionWorkspacePolicy?.allowIssueOverride ?? true,
...executionWorkspacePolicy,
...patch,
},
});
};
const isAbsolutePath = (value: string) => value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value);
const isGitHubRepoUrl = (value: string) => {
@@ -346,6 +371,162 @@ export function ProjectProperties({ project, onUpdate }: ProjectPropertiesProps)
<Separator />
<div className="space-y-1">
<div className="py-1.5 space-y-2">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span>Execution Workspaces</span>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="inline-flex h-4 w-4 items-center justify-center rounded-full border border-border text-[10px] text-muted-foreground hover:text-foreground"
aria-label="Execution workspaces help"
>
?
</button>
</TooltipTrigger>
<TooltipContent side="top">
Project-owned defaults for isolated issue checkouts and execution workspace behavior.
</TooltipContent>
</Tooltip>
</div>
<div className="rounded-md border border-border p-3 space-y-3">
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<div className="text-sm font-medium">Enable isolated issue checkouts</div>
<div className="text-xs text-muted-foreground">
Let issues choose between the projects primary checkout and an isolated execution workspace.
</div>
</div>
{onUpdate ? (
<button
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
executionWorkspacesEnabled ? "bg-green-600" : "bg-muted",
)}
type="button"
onClick={() => updateExecutionWorkspacePolicy({ enabled: !executionWorkspacesEnabled })}
>
<span
className={cn(
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
executionWorkspacesEnabled ? "translate-x-4.5" : "translate-x-0.5",
)}
/>
</button>
) : (
<span className="text-xs text-muted-foreground">
{executionWorkspacesEnabled ? "Enabled" : "Disabled"}
</span>
)}
</div>
{executionWorkspacesEnabled && (
<>
<div className="flex items-center justify-between gap-3 rounded-md border border-border/70 px-3 py-2">
<div className="space-y-0.5">
<div className="text-sm">New issues default to isolated checkout</div>
<div className="text-[11px] text-muted-foreground">
If disabled, new issues stay on the projects primary checkout unless someone opts in.
</div>
</div>
<button
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
executionWorkspaceDefaultMode === "isolated" ? "bg-green-600" : "bg-muted",
)}
type="button"
onClick={() =>
updateExecutionWorkspacePolicy({
defaultMode: executionWorkspaceDefaultMode === "isolated" ? "project_primary" : "isolated",
})}
>
<span
className={cn(
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
executionWorkspaceDefaultMode === "isolated" ? "translate-x-4.5" : "translate-x-0.5",
)}
/>
</button>
</div>
<CollapsibleSection
title="Advanced Checkout Settings"
open={executionWorkspaceAdvancedOpen}
onToggle={() => setExecutionWorkspaceAdvancedOpen((open) => !open)}
>
<div className="space-y-3 pt-1">
<div className="rounded-md border border-border/70 px-3 py-2 text-xs text-muted-foreground">
Host-managed implementation: <span className="text-foreground">Git worktree</span>
</div>
<div>
<div className="mb-1 flex items-center gap-1.5">
<label className="text-xs text-muted-foreground">Base ref</label>
</div>
<DraftInput
value={executionWorkspaceStrategy.baseRef ?? ""}
onCommit={(value) =>
updateExecutionWorkspacePolicy({
workspaceStrategy: {
...executionWorkspaceStrategy,
type: "git_worktree",
baseRef: value || null,
},
})}
immediate
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs font-mono outline-none"
placeholder="origin/main"
/>
</div>
<div>
<div className="mb-1 flex items-center gap-1.5">
<label className="text-xs text-muted-foreground">Branch template</label>
</div>
<DraftInput
value={executionWorkspaceStrategy.branchTemplate ?? ""}
onCommit={(value) =>
updateExecutionWorkspacePolicy({
workspaceStrategy: {
...executionWorkspaceStrategy,
type: "git_worktree",
branchTemplate: value || null,
},
})}
immediate
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs font-mono outline-none"
placeholder="{{issue.identifier}}-{{slug}}"
/>
</div>
<div>
<div className="mb-1 flex items-center gap-1.5">
<label className="text-xs text-muted-foreground">Worktree parent dir</label>
</div>
<DraftInput
value={executionWorkspaceStrategy.worktreeParentDir ?? ""}
onCommit={(value) =>
updateExecutionWorkspacePolicy({
workspaceStrategy: {
...executionWorkspaceStrategy,
type: "git_worktree",
worktreeParentDir: value || null,
},
})}
immediate
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs font-mono outline-none"
placeholder=".paperclip/worktrees"
/>
</div>
<p className="text-[11px] text-muted-foreground">
Runtime services stay under Paperclip control and are not configured here yet.
</p>
</div>
</CollapsibleSection>
</>
)}
</div>
</div>
<Separator />
<div className="py-1.5 space-y-2">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
<span>Workspaces</span>