updating paths
This commit is contained in:
@@ -175,27 +175,27 @@ describe("worktree helpers", () => {
|
|||||||
it("rebinds same-repo workspace paths onto the current worktree root", () => {
|
it("rebinds same-repo workspace paths onto the current worktree root", () => {
|
||||||
expect(
|
expect(
|
||||||
rebindWorkspaceCwd({
|
rebindWorkspaceCwd({
|
||||||
sourceRepoRoot: "/Users/nmurray/paperclip",
|
sourceRepoRoot: "/Users/example/paperclip",
|
||||||
targetRepoRoot: "/Users/nmurray/paperclip-pr-432",
|
targetRepoRoot: "/Users/example/paperclip-pr-432",
|
||||||
workspaceCwd: "/Users/nmurray/paperclip",
|
workspaceCwd: "/Users/example/paperclip",
|
||||||
}),
|
}),
|
||||||
).toBe("/Users/nmurray/paperclip-pr-432");
|
).toBe("/Users/example/paperclip-pr-432");
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
rebindWorkspaceCwd({
|
rebindWorkspaceCwd({
|
||||||
sourceRepoRoot: "/Users/nmurray/paperclip",
|
sourceRepoRoot: "/Users/example/paperclip",
|
||||||
targetRepoRoot: "/Users/nmurray/paperclip-pr-432",
|
targetRepoRoot: "/Users/example/paperclip-pr-432",
|
||||||
workspaceCwd: "/Users/nmurray/paperclip/packages/db",
|
workspaceCwd: "/Users/example/paperclip/packages/db",
|
||||||
}),
|
}),
|
||||||
).toBe("/Users/nmurray/paperclip-pr-432/packages/db");
|
).toBe("/Users/example/paperclip-pr-432/packages/db");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not rebind paths outside the source repo root", () => {
|
it("does not rebind paths outside the source repo root", () => {
|
||||||
expect(
|
expect(
|
||||||
rebindWorkspaceCwd({
|
rebindWorkspaceCwd({
|
||||||
sourceRepoRoot: "/Users/nmurray/paperclip",
|
sourceRepoRoot: "/Users/example/paperclip",
|
||||||
targetRepoRoot: "/Users/nmurray/paperclip-pr-432",
|
targetRepoRoot: "/Users/example/paperclip-pr-432",
|
||||||
workspaceCwd: "/Users/nmurray/other-project",
|
workspaceCwd: "/Users/example/other-project",
|
||||||
}),
|
}),
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
62
doc/experimental/issue-worktree-support.md
Normal file
62
doc/experimental/issue-worktree-support.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Issue worktree support
|
||||||
|
|
||||||
|
Status: experimental, runtime-only, not shipping as a user-facing feature yet.
|
||||||
|
|
||||||
|
This branch contains the runtime and seeding work needed for issue-scoped worktrees:
|
||||||
|
|
||||||
|
- project execution workspace policy support
|
||||||
|
- issue-level execution workspace settings
|
||||||
|
- git worktree realization for isolated issue execution
|
||||||
|
- optional command-based worktree provisioning
|
||||||
|
- seeded worktree fixes for secrets key compatibility
|
||||||
|
- seeded project workspace rebinding to the current git worktree
|
||||||
|
|
||||||
|
We are intentionally not shipping the UI for this yet. The runtime code remains in place, but the main UI entrypoints are hard-gated off for now.
|
||||||
|
|
||||||
|
## What works today
|
||||||
|
|
||||||
|
- projects can carry execution workspace policy in the backend
|
||||||
|
- issues can carry execution workspace settings in the backend
|
||||||
|
- heartbeat execution can realize isolated git worktrees
|
||||||
|
- runtime can run a project-defined provision command inside the derived worktree
|
||||||
|
- seeded worktree instances can keep local-encrypted secrets working
|
||||||
|
- seeded worktree instances can rebind same-repo project workspace paths onto the current git worktree
|
||||||
|
|
||||||
|
## Hidden UI entrypoints
|
||||||
|
|
||||||
|
These are the current user-facing UI surfaces for the feature, now intentionally disabled:
|
||||||
|
|
||||||
|
- project settings:
|
||||||
|
- `ui/src/components/ProjectProperties.tsx`
|
||||||
|
- execution workspace policy controls
|
||||||
|
- git worktree base ref / branch template / parent dir
|
||||||
|
- provision / teardown command inputs
|
||||||
|
|
||||||
|
- issue creation:
|
||||||
|
- `ui/src/components/NewIssueDialog.tsx`
|
||||||
|
- isolated issue checkout toggle
|
||||||
|
- defaulting issue execution workspace settings from project policy
|
||||||
|
|
||||||
|
- issue editing:
|
||||||
|
- `ui/src/components/IssueProperties.tsx`
|
||||||
|
- issue-level workspace mode toggle
|
||||||
|
- defaulting issue execution workspace settings when project changes
|
||||||
|
|
||||||
|
- agent/runtime settings:
|
||||||
|
- `ui/src/adapters/runtime-json-fields.tsx`
|
||||||
|
- runtime services JSON field, which is part of the broader workspace-runtime support surface
|
||||||
|
|
||||||
|
## Why the UI is hidden
|
||||||
|
|
||||||
|
- the runtime behavior is still being validated
|
||||||
|
- the workflow and operator ergonomics are not final
|
||||||
|
- we do not want to expose a partially-baked user-facing feature in issues, projects, or settings
|
||||||
|
|
||||||
|
## Re-enable plan
|
||||||
|
|
||||||
|
When this is ready to ship:
|
||||||
|
|
||||||
|
- re-enable the gated UI sections in the files above
|
||||||
|
- review wording and defaults for project and issue controls
|
||||||
|
- decide which agent/runtime settings should remain advanced-only
|
||||||
|
- add end-to-end product-level verification for the full UI workflow
|
||||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -14,9 +14,6 @@ importers:
|
|||||||
'@playwright/test':
|
'@playwright/test':
|
||||||
specifier: ^1.58.2
|
specifier: ^1.58.2
|
||||||
version: 1.58.2
|
version: 1.58.2
|
||||||
cross-env:
|
|
||||||
specifier: ^10.1.0
|
|
||||||
version: 10.1.0
|
|
||||||
esbuild:
|
esbuild:
|
||||||
specifier: ^0.27.3
|
specifier: ^0.27.3
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
@@ -71,9 +68,6 @@ importers:
|
|||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: 0.38.4
|
specifier: 0.38.4
|
||||||
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
|
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(kysely@0.28.11)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
|
||||||
embedded-postgres:
|
|
||||||
specifier: ^18.1.0-beta.16
|
|
||||||
version: 18.1.0-beta.16
|
|
||||||
picocolors:
|
picocolors:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -327,9 +321,6 @@ importers:
|
|||||||
'@types/ws':
|
'@types/ws':
|
||||||
specifier: ^8.18.1
|
specifier: ^8.18.1
|
||||||
version: 8.18.1
|
version: 8.18.1
|
||||||
cross-env:
|
|
||||||
specifier: ^10.1.0
|
|
||||||
version: 10.1.0
|
|
||||||
supertest:
|
supertest:
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.2.2
|
version: 7.2.2
|
||||||
@@ -998,9 +989,6 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@epic-web/invariant@1.0.0':
|
|
||||||
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
|
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
||||||
deprecated: 'Merged into tsx: https://tsx.is'
|
deprecated: 'Merged into tsx: https://tsx.is'
|
||||||
@@ -3436,11 +3424,6 @@ packages:
|
|||||||
crelt@1.0.6:
|
crelt@1.0.6:
|
||||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||||
|
|
||||||
cross-env@10.1.0:
|
|
||||||
resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
|
|
||||||
engines: {node: '>=20'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -6758,8 +6741,6 @@ snapshots:
|
|||||||
'@embedded-postgres/windows-x64@18.1.0-beta.16':
|
'@embedded-postgres/windows-x64@18.1.0-beta.16':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@epic-web/invariant@1.0.0': {}
|
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.18.20
|
esbuild: 0.18.20
|
||||||
@@ -9274,11 +9255,6 @@ snapshots:
|
|||||||
|
|
||||||
crelt@1.0.6: {}
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
cross-env@10.1.0:
|
|
||||||
dependencies:
|
|
||||||
'@epic-web/invariant': 1.0.0
|
|
||||||
cross-spawn: 7.0.6
|
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { useEffect, useState } from "react";
|
|||||||
import type { AdapterConfigFieldsProps } from "./types";
|
import type { AdapterConfigFieldsProps } from "./types";
|
||||||
import { Field, help } from "../components/agent-config-primitives";
|
import { Field, help } from "../components/agent-config-primitives";
|
||||||
|
|
||||||
|
// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship.
|
||||||
|
const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false;
|
||||||
|
|
||||||
const inputClass =
|
const inputClass =
|
||||||
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
|
"w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40";
|
||||||
|
|
||||||
@@ -57,6 +60,10 @@ export function RuntimeServicesJsonField({
|
|||||||
config,
|
config,
|
||||||
mark,
|
mark,
|
||||||
}: JsonFieldProps) {
|
}: JsonFieldProps) {
|
||||||
|
if (!SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const existing = formatJsonObject(config.workspaceRuntime);
|
const existing = formatJsonObject(config.workspaceRuntime);
|
||||||
const [draft, setDraft] = useState(existing);
|
const [draft, setDraft] = useState(existing);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|||||||
import { User, Hexagon, ArrowUpRight, Tag, Plus, Trash2 } from "lucide-react";
|
import { User, Hexagon, ArrowUpRight, Tag, Plus, Trash2 } from "lucide-react";
|
||||||
import { AgentIcon } from "./AgentIconPicker";
|
import { AgentIcon } from "./AgentIconPicker";
|
||||||
|
|
||||||
|
// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship.
|
||||||
|
const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false;
|
||||||
|
|
||||||
interface IssuePropertiesProps {
|
interface IssuePropertiesProps {
|
||||||
issue: Issue;
|
issue: Issue;
|
||||||
onUpdate: (data: Record<string, unknown>) => void;
|
onUpdate: (data: Record<string, unknown>) => void;
|
||||||
@@ -179,7 +182,9 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
|||||||
const currentProject = issue.projectId
|
const currentProject = issue.projectId
|
||||||
? orderedProjects.find((project) => project.id === issue.projectId) ?? null
|
? orderedProjects.find((project) => project.id === issue.projectId) ?? null
|
||||||
: null;
|
: null;
|
||||||
const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null;
|
const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI
|
||||||
|
? currentProject?.executionWorkspacePolicy ?? null
|
||||||
|
: null;
|
||||||
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
|
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
|
||||||
const usesIsolatedExecutionWorkspace = issue.executionWorkspaceSettings?.mode === "isolated"
|
const usesIsolatedExecutionWorkspace = issue.executionWorkspaceSettings?.mode === "isolated"
|
||||||
? true
|
? true
|
||||||
@@ -435,7 +440,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
projectId: p.id,
|
projectId: p.id,
|
||||||
executionWorkspaceSettings: p.executionWorkspacePolicy?.enabled
|
executionWorkspaceSettings: SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI && p.executionWorkspacePolicy?.enabled
|
||||||
? { mode: p.executionWorkspacePolicy.defaultMode === "isolated" ? "isolated" : "project_primary" }
|
? { mode: p.executionWorkspacePolicy.defaultMode === "isolated" ? "isolated" : "project_primary" }
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySel
|
|||||||
|
|
||||||
const DRAFT_KEY = "paperclip:issue-draft";
|
const DRAFT_KEY = "paperclip:issue-draft";
|
||||||
const DEBOUNCE_MS = 800;
|
const DEBOUNCE_MS = 800;
|
||||||
|
// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship.
|
||||||
|
const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false;
|
||||||
|
|
||||||
/** Return black or white hex based on background luminance (WCAG perceptual weights). */
|
/** Return black or white hex based on background luminance (WCAG perceptual weights). */
|
||||||
function getContrastTextColor(hexColor: string): string {
|
function getContrastTextColor(hexColor: string): string {
|
||||||
@@ -426,7 +428,9 @@ export function NewIssueDialog() {
|
|||||||
chrome: assigneeChrome,
|
chrome: assigneeChrome,
|
||||||
});
|
});
|
||||||
const selectedProject = orderedProjects.find((project) => project.id === projectId);
|
const selectedProject = orderedProjects.find((project) => project.id === projectId);
|
||||||
const executionWorkspacePolicy = selectedProject?.executionWorkspacePolicy;
|
const executionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI
|
||||||
|
? selectedProject?.executionWorkspacePolicy
|
||||||
|
: null;
|
||||||
const executionWorkspaceSettings = executionWorkspacePolicy?.enabled
|
const executionWorkspaceSettings = executionWorkspacePolicy?.enabled
|
||||||
? {
|
? {
|
||||||
mode: useIsolatedExecutionWorkspace ? "isolated" : "project_primary",
|
mode: useIsolatedExecutionWorkspace ? "isolated" : "project_primary",
|
||||||
@@ -472,7 +476,9 @@ export function NewIssueDialog() {
|
|||||||
const currentPriority = priorities.find((p) => p.value === priority);
|
const currentPriority = priorities.find((p) => p.value === priority);
|
||||||
const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId);
|
const currentAssignee = (agents ?? []).find((a) => a.id === assigneeId);
|
||||||
const currentProject = orderedProjects.find((project) => project.id === projectId);
|
const currentProject = orderedProjects.find((project) => project.id === projectId);
|
||||||
const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null;
|
const currentProjectExecutionWorkspacePolicy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI
|
||||||
|
? currentProject?.executionWorkspacePolicy ?? null
|
||||||
|
: null;
|
||||||
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
|
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
|
||||||
const assigneeOptionsTitle =
|
const assigneeOptionsTitle =
|
||||||
assigneeAdapterType === "claude_local"
|
assigneeAdapterType === "claude_local"
|
||||||
@@ -514,7 +520,7 @@ export function NewIssueDialog() {
|
|||||||
const handleProjectChange = useCallback((nextProjectId: string) => {
|
const handleProjectChange = useCallback((nextProjectId: string) => {
|
||||||
setProjectId(nextProjectId);
|
setProjectId(nextProjectId);
|
||||||
const nextProject = orderedProjects.find((project) => project.id === nextProjectId);
|
const nextProject = orderedProjects.find((project) => project.id === nextProjectId);
|
||||||
const policy = nextProject?.executionWorkspacePolicy;
|
const policy = SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI ? nextProject?.executionWorkspacePolicy : null;
|
||||||
executionWorkspaceDefaultProjectId.current = nextProjectId || null;
|
executionWorkspaceDefaultProjectId.current = nextProjectId || null;
|
||||||
setUseIsolatedExecutionWorkspace(Boolean(policy?.enabled && policy.defaultMode === "isolated"));
|
setUseIsolatedExecutionWorkspace(Boolean(policy?.enabled && policy.defaultMode === "isolated"));
|
||||||
}, [orderedProjects]);
|
}, [orderedProjects]);
|
||||||
@@ -527,7 +533,11 @@ export function NewIssueDialog() {
|
|||||||
if (!project) return;
|
if (!project) return;
|
||||||
executionWorkspaceDefaultProjectId.current = projectId;
|
executionWorkspaceDefaultProjectId.current = projectId;
|
||||||
setUseIsolatedExecutionWorkspace(
|
setUseIsolatedExecutionWorkspace(
|
||||||
Boolean(project.executionWorkspacePolicy?.enabled && project.executionWorkspacePolicy.defaultMode === "isolated"),
|
Boolean(
|
||||||
|
SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI &&
|
||||||
|
project.executionWorkspacePolicy?.enabled &&
|
||||||
|
project.executionWorkspacePolicy.defaultMode === "isolated",
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}, [newIssueOpen, orderedProjects, projectId]);
|
}, [newIssueOpen, orderedProjects, projectId]);
|
||||||
const modelOverrideOptions = useMemo<InlineEntityOption[]>(
|
const modelOverrideOptions = useMemo<InlineEntityOption[]>(
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ const PROJECT_STATUSES = [
|
|||||||
{ value: "cancelled", label: "Cancelled" },
|
{ value: "cancelled", label: "Cancelled" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// TODO(issue-worktree-support): re-enable this UI once the workflow is ready to ship.
|
||||||
|
const SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI = false;
|
||||||
|
|
||||||
interface ProjectPropertiesProps {
|
interface ProjectPropertiesProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
onUpdate?: (data: Record<string, unknown>) => void;
|
onUpdate?: (data: Record<string, unknown>) => void;
|
||||||
@@ -707,6 +710,8 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{SHOW_EXPERIMENTAL_ISSUE_WORKTREE_UI && (
|
||||||
|
<>
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
|
|
||||||
<div className="py-1.5 space-y-2">
|
<div className="py-1.5 space-y-2">
|
||||||
@@ -945,6 +950,8 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user