2026-02-20 15:00:56 -06:00
|
|
|
import { useState } from "react";
|
2026-03-02 16:44:03 -06:00
|
|
|
import { Link } from "@/lib/router";
|
2026-02-25 08:38:46 -06:00
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
2026-03-03 08:45:26 -06:00
|
|
|
import type { Project } from "@paperclipai/shared";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { StatusBadge } from "./StatusBadge";
|
2026-03-05 19:00:23 -06:00
|
|
|
import { cn, formatDate } from "../lib/utils";
|
2026-02-20 15:00:56 -06:00
|
|
|
import { goalsApi } from "../api/goals";
|
2026-03-17 09:24:28 -05:00
|
|
|
import { instanceSettingsApi } from "../api/instanceSettings";
|
2026-02-25 08:38:46 -06:00
|
|
|
import { projectsApi } from "../api/projects";
|
2026-02-20 15:00:56 -06:00
|
|
|
import { useCompany } from "../context/CompanyContext";
|
|
|
|
|
import { queryKeys } from "../lib/queryKeys";
|
2026-03-05 19:00:23 -06:00
|
|
|
import { statusBadge, statusBadgeDefault } from "../lib/status-colors";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { Separator } from "@/components/ui/separator";
|
2026-02-20 15:00:56 -06:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
2026-02-25 21:36:06 -06:00
|
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
2026-03-14 17:47:53 -05:00
|
|
|
import { AlertCircle, Archive, ArchiveRestore, Check, ExternalLink, Github, Loader2, Plus, Trash2, X } from "lucide-react";
|
2026-03-02 16:09:07 -06:00
|
|
|
import { ChoosePathButton } from "./PathInstructionsModal";
|
2026-04-04 10:00:39 -05:00
|
|
|
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
2026-03-10 10:04:08 -05:00
|
|
|
import { DraftInput } from "./agent-config-primitives";
|
|
|
|
|
import { InlineEditor } from "./InlineEditor";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-03-05 19:00:23 -06:00
|
|
|
const PROJECT_STATUSES = [
|
|
|
|
|
{ value: "backlog", label: "Backlog" },
|
|
|
|
|
{ value: "planned", label: "Planned" },
|
|
|
|
|
{ value: "in_progress", label: "In Progress" },
|
|
|
|
|
{ value: "completed", label: "Completed" },
|
|
|
|
|
{ value: "cancelled", label: "Cancelled" },
|
|
|
|
|
];
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
interface ProjectPropertiesProps {
|
|
|
|
|
project: Project;
|
2026-02-20 10:32:32 -06:00
|
|
|
onUpdate?: (data: Record<string, unknown>) => void;
|
2026-03-10 10:04:08 -05:00
|
|
|
onFieldUpdate?: (field: ProjectConfigFieldKey, data: Record<string, unknown>) => void;
|
|
|
|
|
getFieldSaveState?: (field: ProjectConfigFieldKey) => ProjectFieldSaveState;
|
2026-03-14 17:47:53 -05:00
|
|
|
onArchive?: (archived: boolean) => void;
|
|
|
|
|
archivePending?: boolean;
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 10:04:08 -05:00
|
|
|
export type ProjectFieldSaveState = "idle" | "saving" | "saved" | "error";
|
|
|
|
|
export type ProjectConfigFieldKey =
|
|
|
|
|
| "name"
|
|
|
|
|
| "description"
|
|
|
|
|
| "status"
|
|
|
|
|
| "goals"
|
|
|
|
|
| "execution_workspace_enabled"
|
|
|
|
|
| "execution_workspace_default_mode"
|
|
|
|
|
| "execution_workspace_base_ref"
|
|
|
|
|
| "execution_workspace_branch_template"
|
2026-03-10 12:42:36 -05:00
|
|
|
| "execution_workspace_worktree_parent_dir"
|
|
|
|
|
| "execution_workspace_provision_command"
|
|
|
|
|
| "execution_workspace_teardown_command";
|
2026-03-10 10:04:08 -05:00
|
|
|
|
|
|
|
|
function SaveIndicator({ state }: { state: ProjectFieldSaveState }) {
|
|
|
|
|
if (state === "saving") {
|
|
|
|
|
return (
|
|
|
|
|
<span className="inline-flex items-center gap-1 text-[11px] text-muted-foreground">
|
|
|
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
|
|
|
Saving
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (state === "saved") {
|
|
|
|
|
return (
|
|
|
|
|
<span className="inline-flex items-center gap-1 text-[11px] text-green-600 dark:text-green-400">
|
|
|
|
|
<Check className="h-3 w-3" />
|
|
|
|
|
Saved
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (state === "error") {
|
|
|
|
|
return (
|
|
|
|
|
<span className="inline-flex items-center gap-1 text-[11px] text-destructive">
|
|
|
|
|
<AlertCircle className="h-3 w-3" />
|
|
|
|
|
Failed
|
|
|
|
|
</span>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function FieldLabel({
|
|
|
|
|
label,
|
|
|
|
|
state,
|
|
|
|
|
}: {
|
|
|
|
|
label: string;
|
|
|
|
|
state: ProjectFieldSaveState;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center gap-1.5">
|
|
|
|
|
<span className="text-xs text-muted-foreground">{label}</span>
|
|
|
|
|
<SaveIndicator state={state} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function PropertyRow({
|
|
|
|
|
label,
|
|
|
|
|
children,
|
|
|
|
|
alignStart = false,
|
|
|
|
|
valueClassName = "",
|
|
|
|
|
}: {
|
|
|
|
|
label: React.ReactNode;
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
alignStart?: boolean;
|
|
|
|
|
valueClassName?: string;
|
|
|
|
|
}) {
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
return (
|
2026-03-10 10:04:08 -05:00
|
|
|
<div className={cn("flex gap-3 py-1.5", alignStart ? "items-start" : "items-center")}>
|
|
|
|
|
<div className="shrink-0 w-20">{label}</div>
|
|
|
|
|
<div className={cn("min-w-0 flex-1", alignStart ? "pt-0.5" : "flex items-center gap-1.5", valueClassName)}>
|
|
|
|
|
{children}
|
|
|
|
|
</div>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 19:00:23 -06:00
|
|
|
function ProjectStatusPicker({ status, onChange }: { status: string; onChange: (status: string) => void }) {
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const colorClass = statusBadge[status] ?? statusBadgeDefault;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Popover open={open} onOpenChange={setOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
className={cn(
|
|
|
|
|
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium whitespace-nowrap shrink-0 cursor-pointer hover:opacity-80 transition-opacity",
|
|
|
|
|
colorClass,
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{status.replace("_", " ")}
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-40 p-1" align="start">
|
|
|
|
|
{PROJECT_STATUSES.map((s) => (
|
|
|
|
|
<Button
|
|
|
|
|
key={s.value}
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className={cn("w-full justify-start gap-2 text-xs", s.value === status && "bg-accent")}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onChange(s.value);
|
|
|
|
|
setOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{s.label}
|
|
|
|
|
</Button>
|
|
|
|
|
))}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 08:16:24 -05:00
|
|
|
function ArchiveDangerZone({
|
|
|
|
|
project,
|
|
|
|
|
onArchive,
|
|
|
|
|
archivePending,
|
|
|
|
|
}: {
|
|
|
|
|
project: Project;
|
|
|
|
|
onArchive: (archived: boolean) => void;
|
|
|
|
|
archivePending?: boolean;
|
|
|
|
|
}) {
|
|
|
|
|
const [confirming, setConfirming] = useState(false);
|
|
|
|
|
const isArchive = !project.archivedAt;
|
|
|
|
|
const action = isArchive ? "Archive" : "Unarchive";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-3 rounded-md border border-destructive/40 bg-destructive/5 px-4 py-4">
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{isArchive
|
|
|
|
|
? "Archive this project to hide it from the sidebar and project selectors."
|
|
|
|
|
: "Unarchive this project to restore it in the sidebar and project selectors."}
|
|
|
|
|
</p>
|
|
|
|
|
{archivePending ? (
|
|
|
|
|
<Button size="sm" variant="destructive" disabled>
|
|
|
|
|
<Loader2 className="h-3 w-3 animate-spin mr-1" />
|
|
|
|
|
{isArchive ? "Archiving..." : "Unarchiving..."}
|
|
|
|
|
</Button>
|
|
|
|
|
) : confirming ? (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-sm text-destructive font-medium">
|
|
|
|
|
{action} “{project.name}”?
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setConfirming(false);
|
|
|
|
|
onArchive(isArchive);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Confirm
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setConfirming(false)}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={() => setConfirming(true)}
|
|
|
|
|
>
|
|
|
|
|
{isArchive ? (
|
|
|
|
|
<><Archive className="h-3 w-3 mr-1" />{action} project</>
|
|
|
|
|
) : (
|
|
|
|
|
<><ArchiveRestore className="h-3 w-3 mr-1" />{action} project</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-14 17:47:53 -05:00
|
|
|
export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState, onArchive, archivePending }: ProjectPropertiesProps) {
|
2026-02-20 15:00:56 -06:00
|
|
|
const { selectedCompanyId } = useCompany();
|
2026-02-25 08:38:46 -06:00
|
|
|
const queryClient = useQueryClient();
|
2026-02-20 15:00:56 -06:00
|
|
|
const [goalOpen, setGoalOpen] = useState(false);
|
2026-03-10 09:03:31 -05:00
|
|
|
const [executionWorkspaceAdvancedOpen, setExecutionWorkspaceAdvancedOpen] = useState(false);
|
2026-02-25 21:36:06 -06:00
|
|
|
const [workspaceMode, setWorkspaceMode] = useState<"local" | "repo" | null>(null);
|
2026-02-25 08:38:46 -06:00
|
|
|
const [workspaceCwd, setWorkspaceCwd] = useState("");
|
|
|
|
|
const [workspaceRepoUrl, setWorkspaceRepoUrl] = useState("");
|
2026-02-25 21:36:06 -06:00
|
|
|
const [workspaceError, setWorkspaceError] = useState<string | null>(null);
|
2026-02-20 15:00:56 -06:00
|
|
|
|
2026-03-10 10:04:08 -05:00
|
|
|
const commitField = (field: ProjectConfigFieldKey, data: Record<string, unknown>) => {
|
|
|
|
|
if (onFieldUpdate) {
|
|
|
|
|
onFieldUpdate(field, data);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
onUpdate?.(data);
|
|
|
|
|
};
|
|
|
|
|
const fieldState = (field: ProjectConfigFieldKey): ProjectFieldSaveState => getFieldSaveState?.(field) ?? "idle";
|
|
|
|
|
|
2026-02-20 15:00:56 -06:00
|
|
|
const { data: allGoals } = useQuery({
|
|
|
|
|
queryKey: queryKeys.goals.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => goalsApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId,
|
|
|
|
|
});
|
2026-03-17 09:24:28 -05:00
|
|
|
const { data: experimentalSettings } = useQuery({
|
|
|
|
|
queryKey: queryKeys.instance.experimentalSettings,
|
|
|
|
|
queryFn: () => instanceSettingsApi.getExperimental(),
|
2026-04-02 11:38:57 -05:00
|
|
|
retry: false,
|
2026-03-17 09:24:28 -05:00
|
|
|
});
|
2026-02-20 15:00:56 -06:00
|
|
|
|
|
|
|
|
const linkedGoalIds = project.goalIds.length > 0
|
|
|
|
|
? project.goalIds
|
|
|
|
|
: project.goalId
|
|
|
|
|
? [project.goalId]
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
const linkedGoals = project.goals.length > 0
|
|
|
|
|
? project.goals
|
|
|
|
|
: linkedGoalIds.map((id) => ({
|
|
|
|
|
id,
|
|
|
|
|
title: allGoals?.find((g) => g.id === id)?.title ?? id.slice(0, 8),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const availableGoals = (allGoals ?? []).filter((g) => !linkedGoalIds.includes(g.id));
|
2026-02-25 08:38:46 -06:00
|
|
|
const workspaces = project.workspaces ?? [];
|
2026-03-16 15:56:37 -05:00
|
|
|
const codebase = project.codebase;
|
|
|
|
|
const primaryCodebaseWorkspace = project.primaryWorkspace ?? null;
|
|
|
|
|
const hasAdditionalLegacyWorkspaces = workspaces.some((workspace) => workspace.id !== primaryCodebaseWorkspace?.id);
|
2026-03-10 09:03:31 -05:00
|
|
|
const executionWorkspacePolicy = project.executionWorkspacePolicy ?? null;
|
|
|
|
|
const executionWorkspacesEnabled = executionWorkspacePolicy?.enabled === true;
|
2026-03-17 09:24:28 -05:00
|
|
|
const isolatedWorkspacesEnabled = experimentalSettings?.enableIsolatedWorkspaces === true;
|
2026-03-10 09:03:31 -05:00
|
|
|
const executionWorkspaceDefaultMode =
|
2026-03-13 17:12:25 -05:00
|
|
|
executionWorkspacePolicy?.defaultMode === "isolated_workspace" ? "isolated_workspace" : "shared_workspace";
|
2026-03-10 09:03:31 -05:00
|
|
|
const executionWorkspaceStrategy = executionWorkspacePolicy?.workspaceStrategy ?? {
|
|
|
|
|
type: "git_worktree",
|
|
|
|
|
baseRef: "",
|
|
|
|
|
branchTemplate: "",
|
|
|
|
|
worktreeParentDir: "",
|
|
|
|
|
};
|
2026-02-25 08:38:46 -06:00
|
|
|
|
|
|
|
|
const invalidateProject = () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(project.id) });
|
2026-03-16 19:34:46 -05:00
|
|
|
if (project.urlKey !== project.id) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(project.urlKey) });
|
|
|
|
|
}
|
2026-02-25 08:38:46 -06:00
|
|
|
if (selectedCompanyId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.projects.list(selectedCompanyId) });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createWorkspace = useMutation({
|
|
|
|
|
mutationFn: (data: Record<string, unknown>) => projectsApi.createWorkspace(project.id, data),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setWorkspaceCwd("");
|
|
|
|
|
setWorkspaceRepoUrl("");
|
2026-02-25 21:36:06 -06:00
|
|
|
setWorkspaceMode(null);
|
|
|
|
|
setWorkspaceError(null);
|
2026-02-25 08:38:46 -06:00
|
|
|
invalidateProject();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const removeWorkspace = useMutation({
|
|
|
|
|
mutationFn: (workspaceId: string) => projectsApi.removeWorkspace(project.id, workspaceId),
|
2026-03-16 19:19:38 -05:00
|
|
|
onSuccess: () => {
|
|
|
|
|
setWorkspaceCwd("");
|
|
|
|
|
setWorkspaceRepoUrl("");
|
|
|
|
|
setWorkspaceMode(null);
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
invalidateProject();
|
|
|
|
|
},
|
2026-02-25 08:38:46 -06:00
|
|
|
});
|
2026-03-02 14:21:03 -06:00
|
|
|
const updateWorkspace = useMutation({
|
|
|
|
|
mutationFn: ({ workspaceId, data }: { workspaceId: string; data: Record<string, unknown> }) =>
|
|
|
|
|
projectsApi.updateWorkspace(project.id, workspaceId, data),
|
2026-03-16 18:26:06 -05:00
|
|
|
onSuccess: () => {
|
|
|
|
|
setWorkspaceCwd("");
|
|
|
|
|
setWorkspaceRepoUrl("");
|
|
|
|
|
setWorkspaceMode(null);
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
invalidateProject();
|
|
|
|
|
},
|
2026-03-02 14:21:03 -06:00
|
|
|
});
|
2026-02-20 15:00:56 -06:00
|
|
|
|
|
|
|
|
const removeGoal = (goalId: string) => {
|
2026-03-10 10:04:08 -05:00
|
|
|
if (!onUpdate && !onFieldUpdate) return;
|
|
|
|
|
commitField("goals", { goalIds: linkedGoalIds.filter((id) => id !== goalId) });
|
2026-02-20 15:00:56 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addGoal = (goalId: string) => {
|
2026-03-10 10:04:08 -05:00
|
|
|
if ((!onUpdate && !onFieldUpdate) || linkedGoalIds.includes(goalId)) return;
|
|
|
|
|
commitField("goals", { goalIds: [...linkedGoalIds, goalId] });
|
2026-02-20 15:00:56 -06:00
|
|
|
setGoalOpen(false);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-10 09:03:31 -05:00
|
|
|
const updateExecutionWorkspacePolicy = (patch: Record<string, unknown>) => {
|
2026-03-10 10:04:08 -05:00
|
|
|
if (!onUpdate && !onFieldUpdate) return;
|
|
|
|
|
return {
|
2026-03-10 09:03:31 -05:00
|
|
|
executionWorkspacePolicy: {
|
|
|
|
|
enabled: executionWorkspacesEnabled,
|
|
|
|
|
defaultMode: executionWorkspaceDefaultMode,
|
|
|
|
|
allowIssueOverride: executionWorkspacePolicy?.allowIssueOverride ?? true,
|
|
|
|
|
...executionWorkspacePolicy,
|
|
|
|
|
...patch,
|
|
|
|
|
},
|
2026-03-10 10:04:08 -05:00
|
|
|
};
|
2026-03-10 09:03:31 -05:00
|
|
|
};
|
|
|
|
|
|
2026-02-25 21:36:06 -06:00
|
|
|
const isAbsolutePath = (value: string) => value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value);
|
|
|
|
|
|
2026-04-01 23:21:22 +00:00
|
|
|
const looksLikeRepoUrl = (value: string) => {
|
2026-02-25 21:36:06 -06:00
|
|
|
try {
|
|
|
|
|
const parsed = new URL(value);
|
2026-04-01 21:05:48 +00:00
|
|
|
if (parsed.protocol !== "https:") return false;
|
2026-02-25 21:36:06 -06:00
|
|
|
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
|
|
|
return segments.length >= 2;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-16 20:12:22 -05:00
|
|
|
const isSafeExternalUrl = (value: string | null | undefined) => {
|
|
|
|
|
if (!value) return false;
|
|
|
|
|
try {
|
|
|
|
|
const parsed = new URL(value);
|
|
|
|
|
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-16 15:56:37 -05:00
|
|
|
const formatRepoUrl = (value: string) => {
|
2026-02-25 21:36:06 -06:00
|
|
|
try {
|
|
|
|
|
const parsed = new URL(value);
|
|
|
|
|
const segments = parsed.pathname.split("/").filter(Boolean);
|
2026-03-16 15:56:37 -05:00
|
|
|
if (segments.length < 2) return parsed.host;
|
2026-02-25 21:36:06 -06:00
|
|
|
const owner = segments[0];
|
|
|
|
|
const repo = segments[1]?.replace(/\.git$/i, "");
|
2026-03-16 15:56:37 -05:00
|
|
|
if (!owner || !repo) return parsed.host;
|
|
|
|
|
return `${parsed.host}/${owner}/${repo}`;
|
2026-02-25 21:36:06 -06:00
|
|
|
} catch {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-16 15:56:37 -05:00
|
|
|
const deriveSourceType = (cwd: string | null, repoUrl: string | null) => {
|
|
|
|
|
if (repoUrl) return "git_repo";
|
|
|
|
|
if (cwd) return "local_path";
|
|
|
|
|
return undefined;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const persistCodebase = (patch: { cwd?: string | null; repoUrl?: string | null }) => {
|
|
|
|
|
const nextCwd = patch.cwd !== undefined ? patch.cwd : codebase.localFolder;
|
|
|
|
|
const nextRepoUrl = patch.repoUrl !== undefined ? patch.repoUrl : codebase.repoUrl;
|
|
|
|
|
if (!nextCwd && !nextRepoUrl) {
|
|
|
|
|
if (primaryCodebaseWorkspace) {
|
|
|
|
|
removeWorkspace.mutate(primaryCodebaseWorkspace.id);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data: Record<string, unknown> = {
|
|
|
|
|
...(patch.cwd !== undefined ? { cwd: patch.cwd } : {}),
|
|
|
|
|
...(patch.repoUrl !== undefined ? { repoUrl: patch.repoUrl } : {}),
|
|
|
|
|
...(deriveSourceType(nextCwd, nextRepoUrl) ? { sourceType: deriveSourceType(nextCwd, nextRepoUrl) } : {}),
|
|
|
|
|
isPrimary: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (primaryCodebaseWorkspace) {
|
|
|
|
|
updateWorkspace.mutate({ workspaceId: primaryCodebaseWorkspace.id, data });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createWorkspace.mutate(data);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-25 21:36:06 -06:00
|
|
|
const submitLocalWorkspace = () => {
|
|
|
|
|
const cwd = workspaceCwd.trim();
|
2026-03-16 19:19:38 -05:00
|
|
|
if (!cwd) {
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
persistCodebase({ cwd: null });
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-25 21:36:06 -06:00
|
|
|
if (!isAbsolutePath(cwd)) {
|
|
|
|
|
setWorkspaceError("Local folder must be a full absolute path.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setWorkspaceError(null);
|
2026-03-16 15:56:37 -05:00
|
|
|
persistCodebase({ cwd });
|
2026-02-25 21:36:06 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const submitRepoWorkspace = () => {
|
|
|
|
|
const repoUrl = workspaceRepoUrl.trim();
|
2026-03-16 19:19:38 -05:00
|
|
|
if (!repoUrl) {
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
persistCodebase({ repoUrl: null });
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-01 23:21:22 +00:00
|
|
|
if (!looksLikeRepoUrl(repoUrl)) {
|
2026-04-01 20:42:48 +00:00
|
|
|
setWorkspaceError("Repo must use a valid GitHub or GitHub Enterprise repo URL.");
|
2026-02-25 21:36:06 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setWorkspaceError(null);
|
2026-03-16 15:56:37 -05:00
|
|
|
persistCodebase({ repoUrl });
|
2026-02-25 08:38:46 -06:00
|
|
|
};
|
|
|
|
|
|
2026-03-16 15:56:37 -05:00
|
|
|
const clearLocalWorkspace = () => {
|
2026-03-02 14:21:03 -06:00
|
|
|
const confirmed = window.confirm(
|
2026-03-16 15:56:37 -05:00
|
|
|
codebase.repoUrl
|
2026-03-02 14:21:03 -06:00
|
|
|
? "Clear local folder from this workspace?"
|
|
|
|
|
: "Delete this workspace local folder?",
|
|
|
|
|
);
|
|
|
|
|
if (!confirmed) return;
|
2026-03-16 15:56:37 -05:00
|
|
|
persistCodebase({ cwd: null });
|
2026-03-02 14:21:03 -06:00
|
|
|
};
|
|
|
|
|
|
2026-03-16 15:56:37 -05:00
|
|
|
const clearRepoWorkspace = () => {
|
|
|
|
|
const hasLocalFolder = Boolean(codebase.localFolder);
|
2026-03-02 14:21:03 -06:00
|
|
|
const confirmed = window.confirm(
|
|
|
|
|
hasLocalFolder
|
2026-03-16 15:56:37 -05:00
|
|
|
? "Clear repo from this workspace?"
|
2026-03-02 14:21:03 -06:00
|
|
|
: "Delete this workspace repo?",
|
|
|
|
|
);
|
|
|
|
|
if (!confirmed) return;
|
2026-03-16 19:38:46 -05:00
|
|
|
if (primaryCodebaseWorkspace && hasLocalFolder) {
|
2026-03-02 14:21:03 -06:00
|
|
|
updateWorkspace.mutate({
|
2026-03-16 15:56:37 -05:00
|
|
|
workspaceId: primaryCodebaseWorkspace.id,
|
|
|
|
|
data: { repoUrl: null, repoRef: null, defaultRef: null, sourceType: deriveSourceType(codebase.localFolder, null) },
|
2026-03-02 14:21:03 -06:00
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-03-16 15:56:37 -05:00
|
|
|
persistCodebase({ repoUrl: null });
|
2026-03-02 14:21:03 -06:00
|
|
|
};
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
return (
|
2026-03-10 10:04:08 -05:00
|
|
|
<div>
|
|
|
|
|
<div className="space-y-1 pb-4">
|
|
|
|
|
<PropertyRow label={<FieldLabel label="Name" state={fieldState("name")} />}>
|
|
|
|
|
{onUpdate || onFieldUpdate ? (
|
|
|
|
|
<DraftInput
|
|
|
|
|
value={project.name}
|
|
|
|
|
onCommit={(name) => commitField("name", { name })}
|
|
|
|
|
immediate
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1 text-sm outline-none"
|
|
|
|
|
placeholder="Project name"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-sm">{project.name}</span>
|
|
|
|
|
)}
|
|
|
|
|
</PropertyRow>
|
|
|
|
|
<PropertyRow
|
|
|
|
|
label={<FieldLabel label="Description" state={fieldState("description")} />}
|
|
|
|
|
alignStart
|
|
|
|
|
valueClassName="space-y-0.5"
|
|
|
|
|
>
|
|
|
|
|
{onUpdate || onFieldUpdate ? (
|
|
|
|
|
<InlineEditor
|
|
|
|
|
value={project.description ?? ""}
|
|
|
|
|
onSave={(description) => commitField("description", { description })}
|
fix: allow to remove project description (#2338)
fixes https://github.com/paperclipai/paperclip/issues/2336
## Thinking Path
<!--
Required. Trace your reasoning from the top of the project down to this
specific change. Start with what Paperclip is, then narrow through the
subsystem, the problem, and why this PR exists. Use blockquote style.
Aim for 5–8 steps. See CONTRIBUTING.md for full examples.
-->
- Paperclip allows to manage projects
- During the project creation you can optionally enter a description
- In the project overview or configuration you can edit the description
- However, you cannot remove the description
- The user should be able to remove the project description because it's
an optional property
- This pull request fixes the frontend bug that prevented the user to
remove/clear the project description
## What Changed
<!-- Bullet list of concrete changes. One bullet per logical unit. -->
- project description can be cleared in "project configuration" and
"project overview"
## Verification
<!--
How can a reviewer confirm this works? Include test commands, manual
steps, or both. For UI changes, include before/after screenshots.
-->
In project configuration or project overview:
- In the description field remove/clear the text
## Risks
<!--
What could go wrong? Mention migration safety, breaking changes,
behavioral shifts, or "Low risk" if genuinely minor.
-->
- none
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-06 22:18:38 +02:00
|
|
|
nullable
|
2026-03-10 10:04:08 -05:00
|
|
|
as="p"
|
|
|
|
|
className="text-sm text-muted-foreground"
|
|
|
|
|
placeholder="Add a description..."
|
|
|
|
|
multiline
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{project.description?.trim() || "No description"}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</PropertyRow>
|
|
|
|
|
<PropertyRow label={<FieldLabel label="Status" state={fieldState("status")} />}>
|
|
|
|
|
{onUpdate || onFieldUpdate ? (
|
2026-03-05 19:00:23 -06:00
|
|
|
<ProjectStatusPicker
|
|
|
|
|
status={project.status}
|
2026-03-10 10:04:08 -05:00
|
|
|
onChange={(status) => commitField("status", { status })}
|
2026-03-05 19:00:23 -06:00
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<StatusBadge status={project.status} />
|
|
|
|
|
)}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</PropertyRow>
|
|
|
|
|
{project.leadAgentId && (
|
|
|
|
|
<PropertyRow label="Lead">
|
|
|
|
|
<span className="text-sm font-mono">{project.leadAgentId.slice(0, 8)}</span>
|
|
|
|
|
</PropertyRow>
|
|
|
|
|
)}
|
2026-03-10 10:04:08 -05:00
|
|
|
<PropertyRow
|
|
|
|
|
label={<FieldLabel label="Goals" state={fieldState("goals")} />}
|
|
|
|
|
alignStart
|
|
|
|
|
valueClassName="space-y-2"
|
|
|
|
|
>
|
2026-03-16 18:47:29 -05:00
|
|
|
{linkedGoals.length > 0 && (
|
2026-03-10 10:04:08 -05:00
|
|
|
<div className="flex flex-wrap gap-1.5">
|
|
|
|
|
{linkedGoals.map((goal) => (
|
|
|
|
|
<span
|
|
|
|
|
key={goal.id}
|
|
|
|
|
className="inline-flex items-center gap-1 rounded-md border border-border px-2 py-1 text-xs"
|
|
|
|
|
>
|
|
|
|
|
<Link to={`/goals/${goal.id}`} className="hover:underline max-w-[220px] truncate">
|
|
|
|
|
{goal.title}
|
|
|
|
|
</Link>
|
|
|
|
|
{(onUpdate || onFieldUpdate) && (
|
|
|
|
|
<button
|
|
|
|
|
className="text-muted-foreground hover:text-foreground"
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => removeGoal(goal.id)}
|
|
|
|
|
aria-label={`Remove goal ${goal.title}`}
|
2026-02-20 15:00:56 -06:00
|
|
|
>
|
2026-03-10 10:04:08 -05:00
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
2026-02-20 15:00:56 -06:00
|
|
|
</div>
|
2026-03-10 10:04:08 -05:00
|
|
|
)}
|
|
|
|
|
{(onUpdate || onFieldUpdate) && (
|
|
|
|
|
<Popover open={goalOpen} onOpenChange={setGoalOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="xs"
|
2026-03-16 18:47:29 -05:00
|
|
|
className={cn("h-6 w-fit px-2", linkedGoals.length > 0 && "ml-1")}
|
2026-03-10 10:04:08 -05:00
|
|
|
disabled={availableGoals.length === 0}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-3 w-3 mr-1" />
|
|
|
|
|
Goal
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-56 p-1" align="start">
|
|
|
|
|
{availableGoals.length === 0 ? (
|
|
|
|
|
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
|
|
|
|
All goals linked.
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
availableGoals.map((goal) => (
|
|
|
|
|
<button
|
|
|
|
|
key={goal.id}
|
|
|
|
|
className="flex items-center w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50"
|
|
|
|
|
onClick={() => addGoal(goal.id)}
|
|
|
|
|
>
|
|
|
|
|
{goal.title}
|
|
|
|
|
</button>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
)}
|
|
|
|
|
</PropertyRow>
|
|
|
|
|
<PropertyRow label={<FieldLabel label="Created" state="idle" />}>
|
|
|
|
|
<span className="text-sm">{formatDate(project.createdAt)}</span>
|
|
|
|
|
</PropertyRow>
|
|
|
|
|
<PropertyRow label={<FieldLabel label="Updated" state="idle" />}>
|
|
|
|
|
<span className="text-sm">{formatDate(project.updatedAt)}</span>
|
|
|
|
|
</PropertyRow>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
{project.targetDate && (
|
2026-03-10 10:04:08 -05:00
|
|
|
<PropertyRow label={<FieldLabel label="Target Date" state="idle" />}>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
<span className="text-sm">{formatDate(project.targetDate)}</span>
|
|
|
|
|
</PropertyRow>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-10 10:04:08 -05:00
|
|
|
<Separator className="my-4" />
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-03-10 10:04:08 -05:00
|
|
|
<div className="space-y-1 py-4">
|
|
|
|
|
<div className="space-y-2">
|
2026-02-25 21:36:06 -06:00
|
|
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
2026-03-16 15:56:37 -05:00
|
|
|
<span>Codebase</span>
|
2026-02-25 21:36:06 -06:00
|
|
|
<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"
|
2026-03-16 15:56:37 -05:00
|
|
|
aria-label="Codebase help"
|
2026-02-25 21:36:06 -06:00
|
|
|
>
|
|
|
|
|
?
|
|
|
|
|
</button>
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent side="top">
|
2026-03-16 15:56:37 -05:00
|
|
|
Repo identifies the source of truth. Local folder is the default place agents write code.
|
2026-02-25 21:36:06 -06:00
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
2026-03-16 15:56:37 -05:00
|
|
|
<div className="space-y-2 rounded-md border border-border/70 p-3">
|
2026-02-25 21:36:06 -06:00
|
|
|
<div className="space-y-1">
|
2026-03-16 15:56:37 -05:00
|
|
|
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">Repo</div>
|
|
|
|
|
{codebase.repoUrl ? (
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
2026-03-16 20:12:22 -05:00
|
|
|
{isSafeExternalUrl(codebase.repoUrl) ? (
|
|
|
|
|
<a
|
|
|
|
|
href={codebase.repoUrl}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
className="inline-flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground hover:underline"
|
|
|
|
|
>
|
|
|
|
|
<Github className="h-3 w-3 shrink-0" />
|
|
|
|
|
<span className="truncate">{formatRepoUrl(codebase.repoUrl)}</span>
|
|
|
|
|
<ExternalLink className="h-3 w-3 shrink-0" />
|
|
|
|
|
</a>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="inline-flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
|
|
<Github className="h-3 w-3 shrink-0" />
|
|
|
|
|
<span className="truncate">{codebase.repoUrl}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-16 18:26:06 -05:00
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="xs"
|
|
|
|
|
className="h-6 px-2"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setWorkspaceMode("repo");
|
|
|
|
|
setWorkspaceRepoUrl(codebase.repoUrl ?? "");
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Change repo
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
onClick={clearRepoWorkspace}
|
|
|
|
|
aria-label="Clear repo"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
<div className="text-xs text-muted-foreground">Not set.</div>
|
2026-03-16 15:56:37 -05:00
|
|
|
<Button
|
2026-03-16 18:26:06 -05:00
|
|
|
variant="outline"
|
|
|
|
|
size="xs"
|
|
|
|
|
className="h-6 px-2"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setWorkspaceMode("repo");
|
|
|
|
|
setWorkspaceRepoUrl(codebase.repoUrl ?? "");
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
}}
|
2026-03-16 15:56:37 -05:00
|
|
|
>
|
2026-03-16 18:26:06 -05:00
|
|
|
Set repo
|
2026-03-16 15:56:37 -05:00
|
|
|
</Button>
|
2026-02-25 08:38:46 -06:00
|
|
|
</div>
|
2026-03-16 15:56:37 -05:00
|
|
|
)}
|
2026-02-25 08:38:46 -06:00
|
|
|
</div>
|
2026-03-16 15:56:37 -05:00
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<div className="text-[11px] uppercase tracking-wide text-muted-foreground">Local folder</div>
|
2026-03-16 18:26:06 -05:00
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
<div className="min-w-0 space-y-1">
|
|
|
|
|
<div className="min-w-0 truncate font-mono text-xs text-muted-foreground">
|
|
|
|
|
{codebase.effectiveLocalFolder}
|
|
|
|
|
</div>
|
|
|
|
|
{codebase.origin === "managed_checkout" && (
|
|
|
|
|
<div className="text-[11px] text-muted-foreground">Paperclip-managed folder.</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-1">
|
2026-03-16 15:56:37 -05:00
|
|
|
<Button
|
2026-03-16 18:26:06 -05:00
|
|
|
variant="outline"
|
|
|
|
|
size="xs"
|
|
|
|
|
className="h-6 px-2"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setWorkspaceMode("local");
|
|
|
|
|
setWorkspaceCwd(codebase.localFolder ?? "");
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
}}
|
2026-03-16 15:56:37 -05:00
|
|
|
>
|
2026-03-16 18:26:06 -05:00
|
|
|
{codebase.localFolder ? "Change local folder" : "Set local folder"}
|
2026-03-16 15:56:37 -05:00
|
|
|
</Button>
|
2026-03-16 18:26:06 -05:00
|
|
|
{codebase.localFolder ? (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
onClick={clearLocalWorkspace}
|
|
|
|
|
aria-label="Clear local folder"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
) : null}
|
2026-03-16 15:56:37 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{hasAdditionalLegacyWorkspaces && (
|
|
|
|
|
<div className="text-[11px] text-muted-foreground">
|
|
|
|
|
Additional legacy workspace records exist on this project. Paperclip is using the primary workspace as the codebase view.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{primaryCodebaseWorkspace?.runtimeServices && primaryCodebaseWorkspace.runtimeServices.length > 0 ? (
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{primaryCodebaseWorkspace.runtimeServices.map((service) => (
|
|
|
|
|
<div
|
|
|
|
|
key={service.id}
|
|
|
|
|
className="flex items-center justify-between gap-2 rounded-md border border-border/60 px-2 py-1"
|
|
|
|
|
>
|
|
|
|
|
<div className="min-w-0 space-y-0.5">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-[11px] font-medium">{service.serviceName}</span>
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
"rounded-full px-1.5 py-0.5 text-[10px] uppercase tracking-wide",
|
|
|
|
|
service.status === "running"
|
|
|
|
|
? "bg-green-500/15 text-green-700 dark:text-green-300"
|
|
|
|
|
: service.status === "failed"
|
|
|
|
|
? "bg-red-500/15 text-red-700 dark:text-red-300"
|
|
|
|
|
: "bg-muted text-muted-foreground",
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{service.status}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-[11px] text-muted-foreground">
|
|
|
|
|
{service.url ? (
|
|
|
|
|
<a
|
|
|
|
|
href={service.url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
className="hover:text-foreground hover:underline"
|
|
|
|
|
>
|
|
|
|
|
{service.url}
|
|
|
|
|
</a>
|
|
|
|
|
) : (
|
|
|
|
|
service.command ?? "No URL"
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-[10px] text-muted-foreground whitespace-nowrap">
|
|
|
|
|
{service.lifecycle}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2026-02-25 21:36:06 -06:00
|
|
|
{workspaceMode === "local" && (
|
|
|
|
|
<div className="space-y-1.5 rounded-md border border-border p-2">
|
2026-03-02 16:09:07 -06:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<input
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs font-mono outline-none"
|
|
|
|
|
value={workspaceCwd}
|
|
|
|
|
onChange={(e) => setWorkspaceCwd(e.target.value)}
|
|
|
|
|
placeholder="/absolute/path/to/workspace"
|
|
|
|
|
/>
|
|
|
|
|
<ChoosePathButton />
|
|
|
|
|
</div>
|
2026-02-25 21:36:06 -06:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="xs"
|
|
|
|
|
className="h-6 px-2"
|
2026-03-16 19:19:38 -05:00
|
|
|
disabled={(!workspaceCwd.trim() && !primaryCodebaseWorkspace) || createWorkspace.isPending || updateWorkspace.isPending}
|
2026-02-25 21:36:06 -06:00
|
|
|
onClick={submitLocalWorkspace}
|
|
|
|
|
>
|
|
|
|
|
Save
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="xs"
|
|
|
|
|
className="h-6 px-2"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setWorkspaceMode(null);
|
|
|
|
|
setWorkspaceCwd("");
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{workspaceMode === "repo" && (
|
|
|
|
|
<div className="space-y-1.5 rounded-md border border-border p-2">
|
|
|
|
|
<input
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs outline-none"
|
|
|
|
|
value={workspaceRepoUrl}
|
|
|
|
|
onChange={(e) => setWorkspaceRepoUrl(e.target.value)}
|
|
|
|
|
placeholder="https://github.com/org/repo"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="xs"
|
|
|
|
|
className="h-6 px-2"
|
2026-03-16 19:19:38 -05:00
|
|
|
disabled={(!workspaceRepoUrl.trim() && !primaryCodebaseWorkspace) || createWorkspace.isPending || updateWorkspace.isPending}
|
2026-02-25 21:36:06 -06:00
|
|
|
onClick={submitRepoWorkspace}
|
|
|
|
|
>
|
|
|
|
|
Save
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="xs"
|
|
|
|
|
className="h-6 px-2"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setWorkspaceMode(null);
|
|
|
|
|
setWorkspaceRepoUrl("");
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{workspaceError && (
|
|
|
|
|
<p className="text-xs text-destructive">{workspaceError}</p>
|
|
|
|
|
)}
|
|
|
|
|
{createWorkspace.isError && (
|
|
|
|
|
<p className="text-xs text-destructive">Failed to save workspace.</p>
|
|
|
|
|
)}
|
|
|
|
|
{removeWorkspace.isError && (
|
|
|
|
|
<p className="text-xs text-destructive">Failed to delete workspace.</p>
|
|
|
|
|
)}
|
2026-03-02 14:21:03 -06:00
|
|
|
{updateWorkspace.isError && (
|
|
|
|
|
<p className="text-xs text-destructive">Failed to update workspace.</p>
|
|
|
|
|
)}
|
2026-02-25 08:38:46 -06:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-17 09:24:28 -05:00
|
|
|
{isolatedWorkspacesEnabled ? (
|
|
|
|
|
<>
|
|
|
|
|
<Separator className="my-4" />
|
2026-03-10 10:04:08 -05:00
|
|
|
|
2026-03-17 09:24:28 -05:00
|
|
|
<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>
|
2026-03-10 10:04:08 -05:00
|
|
|
</div>
|
2026-03-17 09:24:28 -05:00
|
|
|
<div className="space-y-3">
|
2026-03-10 10:04:08 -05:00
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<div className="space-y-0.5">
|
2026-03-17 09:24:28 -05:00
|
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
|
|
|
<span>Enable isolated issue checkouts</span>
|
|
|
|
|
<SaveIndicator state={fieldState("execution_workspace_enabled")} />
|
2026-03-10 10:04:08 -05:00
|
|
|
</div>
|
2026-03-17 09:24:28 -05:00
|
|
|
<div className="text-xs text-muted-foreground">
|
|
|
|
|
Let issues choose between the project's primary checkout and an isolated execution workspace.
|
2026-03-10 10:04:08 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-17 09:24:28 -05:00
|
|
|
{onUpdate || onFieldUpdate ? (
|
2026-04-04 10:00:39 -05:00
|
|
|
<ToggleSwitch
|
|
|
|
|
checked={executionWorkspacesEnabled}
|
|
|
|
|
onCheckedChange={() =>
|
2026-03-17 09:24:28 -05:00
|
|
|
commitField(
|
|
|
|
|
"execution_workspace_enabled",
|
|
|
|
|
updateExecutionWorkspacePolicy({ enabled: !executionWorkspacesEnabled })!,
|
|
|
|
|
)}
|
2026-04-04 10:00:39 -05:00
|
|
|
/>
|
2026-03-17 09:24:28 -05:00
|
|
|
) : (
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{executionWorkspacesEnabled ? "Enabled" : "Disabled"}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-03-10 10:04:08 -05:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-17 09:24:28 -05:00
|
|
|
{executionWorkspacesEnabled ? (
|
2026-03-10 10:04:08 -05:00
|
|
|
<div className="space-y-3">
|
2026-03-17 09:24:28 -05:00
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<div className="flex items-center gap-2 text-sm">
|
|
|
|
|
<span>New issues default to isolated checkout</span>
|
|
|
|
|
<SaveIndicator state={fieldState("execution_workspace_default_mode")} />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-[11px] text-muted-foreground">
|
|
|
|
|
If disabled, new issues stay on the project's primary checkout unless someone opts in.
|
|
|
|
|
</div>
|
2026-03-10 10:04:08 -05:00
|
|
|
</div>
|
2026-04-04 10:00:39 -05:00
|
|
|
<ToggleSwitch
|
|
|
|
|
checked={executionWorkspaceDefaultMode === "isolated_workspace"}
|
|
|
|
|
onCheckedChange={() =>
|
2026-03-17 09:24:28 -05:00
|
|
|
commitField(
|
|
|
|
|
"execution_workspace_default_mode",
|
|
|
|
|
updateExecutionWorkspacePolicy({
|
|
|
|
|
defaultMode:
|
|
|
|
|
executionWorkspaceDefaultMode === "isolated_workspace"
|
|
|
|
|
? "shared_workspace"
|
|
|
|
|
: "isolated_workspace",
|
2026-03-10 10:04:08 -05:00
|
|
|
})!,
|
2026-03-17 09:24:28 -05:00
|
|
|
)}
|
2026-04-04 10:00:39 -05:00
|
|
|
/>
|
2026-03-10 10:04:08 -05:00
|
|
|
</div>
|
2026-03-17 09:24:28 -05:00
|
|
|
|
|
|
|
|
<div className="border-t border-border/60 pt-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="flex w-full items-center gap-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
|
|
|
|
|
onClick={() => setExecutionWorkspaceAdvancedOpen((open) => !open)}
|
|
|
|
|
>
|
|
|
|
|
{executionWorkspaceAdvancedOpen
|
|
|
|
|
? "Hide advanced checkout settings"
|
|
|
|
|
: "Show advanced checkout settings"}
|
|
|
|
|
</button>
|
2026-03-10 12:42:36 -05:00
|
|
|
</div>
|
2026-03-17 09:24:28 -05:00
|
|
|
|
|
|
|
|
{executionWorkspaceAdvancedOpen ? (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="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="flex items-center gap-2 text-xs text-muted-foreground">
|
|
|
|
|
<span>Base ref</span>
|
|
|
|
|
<SaveIndicator state={fieldState("execution_workspace_base_ref")} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<DraftInput
|
|
|
|
|
value={executionWorkspaceStrategy.baseRef ?? ""}
|
|
|
|
|
onCommit={(value) =>
|
|
|
|
|
commitField("execution_workspace_base_ref", {
|
|
|
|
|
...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="flex items-center gap-2 text-xs text-muted-foreground">
|
|
|
|
|
<span>Branch template</span>
|
|
|
|
|
<SaveIndicator state={fieldState("execution_workspace_branch_template")} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<DraftInput
|
|
|
|
|
value={executionWorkspaceStrategy.branchTemplate ?? ""}
|
|
|
|
|
onCommit={(value) =>
|
|
|
|
|
commitField("execution_workspace_branch_template", {
|
|
|
|
|
...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="flex items-center gap-2 text-xs text-muted-foreground">
|
|
|
|
|
<span>Worktree parent dir</span>
|
|
|
|
|
<SaveIndicator state={fieldState("execution_workspace_worktree_parent_dir")} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<DraftInput
|
|
|
|
|
value={executionWorkspaceStrategy.worktreeParentDir ?? ""}
|
|
|
|
|
onCommit={(value) =>
|
|
|
|
|
commitField("execution_workspace_worktree_parent_dir", {
|
|
|
|
|
...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>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="mb-1 flex items-center gap-1.5">
|
|
|
|
|
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
|
|
|
<span>Provision command</span>
|
|
|
|
|
<SaveIndicator state={fieldState("execution_workspace_provision_command")} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<DraftInput
|
|
|
|
|
value={executionWorkspaceStrategy.provisionCommand ?? ""}
|
|
|
|
|
onCommit={(value) =>
|
|
|
|
|
commitField("execution_workspace_provision_command", {
|
|
|
|
|
...updateExecutionWorkspacePolicy({
|
|
|
|
|
workspaceStrategy: {
|
|
|
|
|
...executionWorkspaceStrategy,
|
|
|
|
|
type: "git_worktree",
|
|
|
|
|
provisionCommand: value || null,
|
|
|
|
|
},
|
|
|
|
|
})!,
|
|
|
|
|
})}
|
|
|
|
|
immediate
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs font-mono outline-none"
|
|
|
|
|
placeholder="bash ./scripts/provision-worktree.sh"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="mb-1 flex items-center gap-1.5">
|
|
|
|
|
<label className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
|
|
|
<span>Teardown command</span>
|
|
|
|
|
<SaveIndicator state={fieldState("execution_workspace_teardown_command")} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<DraftInput
|
|
|
|
|
value={executionWorkspaceStrategy.teardownCommand ?? ""}
|
|
|
|
|
onCommit={(value) =>
|
|
|
|
|
commitField("execution_workspace_teardown_command", {
|
|
|
|
|
...updateExecutionWorkspacePolicy({
|
|
|
|
|
workspaceStrategy: {
|
|
|
|
|
...executionWorkspaceStrategy,
|
|
|
|
|
type: "git_worktree",
|
|
|
|
|
teardownCommand: value || null,
|
|
|
|
|
},
|
|
|
|
|
})!,
|
|
|
|
|
})}
|
|
|
|
|
immediate
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs font-mono outline-none"
|
|
|
|
|
placeholder="bash ./scripts/teardown-worktree.sh"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-[11px] text-muted-foreground">
|
|
|
|
|
Provision runs inside the derived worktree before agent execution. Teardown is stored here for
|
|
|
|
|
future cleanup flows.
|
|
|
|
|
</p>
|
2026-03-10 12:42:36 -05:00
|
|
|
</div>
|
2026-03-17 09:24:28 -05:00
|
|
|
) : null}
|
2026-03-10 10:04:08 -05:00
|
|
|
</div>
|
2026-03-17 09:24:28 -05:00
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : null}
|
2026-02-25 08:38:46 -06:00
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
2026-03-14 17:47:53 -05:00
|
|
|
|
|
|
|
|
{onArchive && (
|
|
|
|
|
<>
|
|
|
|
|
<Separator className="my-4" />
|
|
|
|
|
<div className="space-y-4 py-4">
|
|
|
|
|
<div className="text-xs font-medium text-destructive uppercase tracking-wide">
|
|
|
|
|
Danger Zone
|
|
|
|
|
</div>
|
2026-03-16 08:16:24 -05:00
|
|
|
<ArchiveDangerZone
|
|
|
|
|
project={project}
|
|
|
|
|
onArchive={onArchive}
|
|
|
|
|
archivePending={archivePending}
|
|
|
|
|
/>
|
2026-03-14 17:47:53 -05:00
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|