Remove the experimental workspace toggle

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta
2026-03-16 18:37:59 -05:00
parent 4220d6e057
commit 7e4aec9379
6 changed files with 12 additions and 147 deletions

View File

@@ -17,7 +17,6 @@ import { PriorityIcon } from "./PriorityIcon";
import { Identity } from "./Identity";
import { formatDate, cn, projectUrl } from "../lib/utils";
import { timeAgo } from "../lib/timeAgo";
import { useExperimentalWorkspacesEnabled } from "../lib/experimentalSettings";
import { Separator } from "@/components/ui/separator";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { User, Hexagon, ArrowUpRight, Tag, Plus, Trash2 } from "lucide-react";
@@ -134,7 +133,6 @@ function PropertyPicker({
export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProps) {
const { selectedCompanyId } = useCompany();
const { enabled: showExperimentalWorkspaceUi } = useExperimentalWorkspacesEnabled();
const queryClient = useQueryClient();
const companyId = issue.companyId ?? selectedCompanyId;
const [assigneeOpen, setAssigneeOpen] = useState(false);
@@ -219,11 +217,8 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
const currentProject = issue.projectId
? orderedProjects.find((project) => project.id === issue.projectId) ?? null
: null;
const currentProjectExecutionWorkspacePolicy = showExperimentalWorkspaceUi
? currentProject?.executionWorkspacePolicy ?? null
: null;
const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null;
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
const currentProjectWorkspaces = currentProject?.workspaces ?? [];
const currentExecutionWorkspaceSelection =
issue.executionWorkspacePreference
?? issue.executionWorkspaceSettings?.mode
@@ -240,7 +235,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
projectWorkspaceId: issue.projectWorkspaceId ?? undefined,
reuseEligible: true,
}),
enabled: Boolean(companyId) && showExperimentalWorkspaceUi && Boolean(issue.projectId),
enabled: Boolean(companyId) && Boolean(issue.projectId),
});
const selectedReusableExecutionWorkspace = (reusableExecutionWorkspaces ?? []).find(
(workspace) => workspace.id === issue.executionWorkspaceId,
@@ -509,10 +504,10 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
const defaultMode = defaultExecutionWorkspaceModeForProject(p);
onUpdate({
projectId: p.id,
projectWorkspaceId: showExperimentalWorkspaceUi ? defaultProjectWorkspaceIdForProject(p) : null,
projectWorkspaceId: defaultProjectWorkspaceIdForProject(p),
executionWorkspaceId: null,
executionWorkspacePreference: showExperimentalWorkspaceUi ? defaultMode : null,
executionWorkspaceSettings: showExperimentalWorkspaceUi && p.executionWorkspacePolicy?.enabled
executionWorkspacePreference: defaultMode,
executionWorkspaceSettings: p.executionWorkspacePolicy?.enabled
? { mode: defaultMode }
: null,
});
@@ -602,28 +597,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
{projectContent}
</PropertyPicker>
{showExperimentalWorkspaceUi && currentProjectWorkspaces.length > 0 && (
<PropertyRow label="Codebase">
<select
className="w-full rounded border border-border bg-transparent px-2 py-1.5 text-xs outline-none"
value={issue.projectWorkspaceId ?? ""}
onChange={(e) =>
onUpdate({
projectWorkspaceId: e.target.value || null,
executionWorkspaceId: null,
})}
>
{currentProjectWorkspaces.map((workspace) => (
<option key={workspace.id} value={workspace.id}>
{workspace.name}
{workspace.isPrimary ? " (default)" : ""}
</option>
))}
</select>
</PropertyRow>
)}
{showExperimentalWorkspaceUi && currentProjectSupportsExecutionWorkspace && (
{currentProjectSupportsExecutionWorkspace && (
<PropertyRow label="Workspace">
<div className="w-full space-y-2">
<select

View File

@@ -51,7 +51,6 @@ import { issueStatusText, issueStatusTextDefault, priorityColor, priorityColorDe
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor";
import { AgentIcon } from "./AgentIconPicker";
import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector";
import { useExperimentalWorkspacesEnabled } from "../lib/experimentalSettings";
const DRAFT_KEY = "paperclip:issue-draft";
const DEBOUNCE_MS = 800;
@@ -280,7 +279,6 @@ function issueExecutionWorkspaceModeForExistingWorkspace(mode: string | null | u
export function NewIssueDialog() {
const { newIssueOpen, newIssueDefaults, closeNewIssue } = useDialog();
const { companies, selectedCompanyId, selectedCompany } = useCompany();
const { enabled: showExperimentalWorkspaceUi } = useExperimentalWorkspacesEnabled();
const queryClient = useQueryClient();
const { pushToast } = useToast();
const [title, setTitle] = useState("");
@@ -339,7 +337,7 @@ export function NewIssueDialog() {
projectWorkspaceId: projectWorkspaceId || undefined,
reuseEligible: true,
}),
enabled: Boolean(effectiveCompanyId) && newIssueOpen && showExperimentalWorkspaceUi && Boolean(projectId),
enabled: Boolean(effectiveCompanyId) && newIssueOpen && Boolean(projectId),
});
const { data: session } = useQuery({
queryKey: queryKeys.auth.session,
@@ -639,9 +637,7 @@ export function NewIssueDialog() {
chrome: assigneeChrome,
});
const selectedProject = orderedProjects.find((project) => project.id === projectId);
const executionWorkspacePolicy = showExperimentalWorkspaceUi
? selectedProject?.executionWorkspacePolicy
: null;
const executionWorkspacePolicy = selectedProject?.executionWorkspacePolicy ?? null;
const selectedReusableExecutionWorkspace = (reusableExecutionWorkspaces ?? []).find(
(workspace) => workspace.id === selectedExecutionWorkspaceId,
);
@@ -749,10 +745,7 @@ export function NewIssueDialog() {
? (agents ?? []).find((a) => a.id === selectedAssigneeAgentId)
: null;
const currentProject = orderedProjects.find((project) => project.id === projectId);
const currentProjectWorkspaces = currentProject?.workspaces ?? [];
const currentProjectExecutionWorkspacePolicy = showExperimentalWorkspaceUi
? currentProject?.executionWorkspacePolicy ?? null
: null;
const currentProjectExecutionWorkspacePolicy = currentProject?.executionWorkspacePolicy ?? null;
const currentProjectSupportsExecutionWorkspace = Boolean(currentProjectExecutionWorkspacePolicy?.enabled);
const selectedReusableExecutionWorkspace = (reusableExecutionWorkspaces ?? []).find(
(workspace) => workspace.id === selectedExecutionWorkspaceId,
@@ -822,7 +815,7 @@ export function NewIssueDialog() {
setProjectWorkspaceId(defaultProjectWorkspaceIdForProject(project));
setExecutionWorkspaceMode(defaultExecutionWorkspaceModeForProject(project));
setSelectedExecutionWorkspaceId("");
}, [newIssueOpen, orderedProjects, projectId, showExperimentalWorkspaceUi]);
}, [newIssueOpen, orderedProjects, projectId]);
const modelOverrideOptions = useMemo<InlineEntityOption[]>(
() => {
return [...(assigneeAdapterModels ?? [])]
@@ -1102,29 +1095,8 @@ export function NewIssueDialog() {
</div>
</div>
{showExperimentalWorkspaceUi && currentProject && (
{currentProject && (
<div className="px-4 pb-2 shrink-0 space-y-2">
{currentProjectWorkspaces.length > 0 && (
<div className="rounded-md border border-border px-3 py-2 space-y-1.5">
<div className="text-xs font-medium">Codebase</div>
<div className="text-[11px] text-muted-foreground">
Choose which project workspace this issue should use.
</div>
<select
className="w-full rounded border border-border bg-transparent px-2 py-1.5 text-xs outline-none"
value={projectWorkspaceId}
onChange={(e) => setProjectWorkspaceId(e.target.value)}
>
{currentProjectWorkspaces.map((workspace) => (
<option key={workspace.id} value={workspace.id}>
{workspace.name}
{workspace.isPrimary ? " (default)" : ""}
</option>
))}
</select>
</div>
)}
{currentProjectSupportsExecutionWorkspace && (
<div className="rounded-md border border-border px-3 py-2 space-y-1.5">
<div className="text-xs font-medium">Execution workspace</div>

View File

@@ -17,7 +17,6 @@ import { AlertCircle, Archive, ArchiveRestore, Check, ExternalLink, Github, Load
import { ChoosePathButton } from "./PathInstructionsModal";
import { DraftInput } from "./agent-config-primitives";
import { InlineEditor } from "./InlineEditor";
import { useExperimentalWorkspacesEnabled } from "../lib/experimentalSettings";
const PROJECT_STATUSES = [
{ value: "backlog", label: "Backlog" },
@@ -152,7 +151,6 @@ function ProjectStatusPicker({ status, onChange }: { status: string; onChange: (
export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSaveState, onArchive, archivePending }: ProjectPropertiesProps) {
const { selectedCompanyId } = useCompany();
const { enabled: showExperimentalWorkspaceUi } = useExperimentalWorkspacesEnabled();
const queryClient = useQueryClient();
const [goalOpen, setGoalOpen] = useState(false);
const [executionWorkspaceAdvancedOpen, setExecutionWorkspaceAdvancedOpen] = useState(false);
@@ -749,9 +747,6 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
)}
</div>
{/* PAP-525: workspace UI gated by useExperimentalWorkspacesEnabled() */}
{showExperimentalWorkspaceUi && (
<>
<Separator className="my-4" />
<div className="py-1.5 space-y-2">
@@ -990,8 +985,6 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
)}
</div>
</div>
</>
)}
</div>

View File

@@ -1,39 +0,0 @@
import { useEffect, useState } from "react";
const WORKSPACES_KEY = "paperclip:experimental:workspaces";
export function loadExperimentalWorkspacesEnabled(): boolean {
if (typeof window === "undefined") return false;
return window.localStorage.getItem(WORKSPACES_KEY) === "true";
}
export function saveExperimentalWorkspacesEnabled(enabled: boolean) {
if (typeof window === "undefined") return;
window.localStorage.setItem(WORKSPACES_KEY, enabled ? "true" : "false");
window.dispatchEvent(new CustomEvent("paperclip:experimental:workspaces", { detail: enabled }));
}
export function useExperimentalWorkspacesEnabled() {
const [enabled, setEnabled] = useState(loadExperimentalWorkspacesEnabled);
useEffect(() => {
const handleStorage = (event: StorageEvent) => {
if (event.key && event.key !== WORKSPACES_KEY) return;
setEnabled(loadExperimentalWorkspacesEnabled());
};
const handleCustom = () => setEnabled(loadExperimentalWorkspacesEnabled());
window.addEventListener("storage", handleStorage);
window.addEventListener("paperclip:experimental:workspaces", handleCustom as EventListener);
return () => {
window.removeEventListener("storage", handleStorage);
window.removeEventListener("paperclip:experimental:workspaces", handleCustom as EventListener);
};
}, []);
const update = (next: boolean) => {
saveExperimentalWorkspacesEnabled(next);
setEnabled(next);
};
return { enabled, setEnabled: update };
}

View File

@@ -12,7 +12,6 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { queryKeys } from "../lib/queryKeys";
import { formatDateTime, relativeTime } from "../lib/utils";
import { useExperimentalWorkspacesEnabled } from "../lib/experimentalSettings";
function asRecord(value: unknown): Record<string, unknown> | null {
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
@@ -31,7 +30,6 @@ export function InstanceSettings() {
const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
const [actionError, setActionError] = useState<string | null>(null);
const { enabled: workspacesEnabled, setEnabled: setWorkspacesEnabled } = useExperimentalWorkspacesEnabled();
useEffect(() => {
setBreadcrumbs([
@@ -112,34 +110,6 @@ export function InstanceSettings() {
return (
<div className="max-w-5xl space-y-6">
<div className="space-y-3 rounded-lg border border-border bg-card p-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Settings className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold">Experimental</h2>
</div>
<p className="text-sm text-muted-foreground">
UI-only feature flags for in-progress product surfaces.
</p>
</div>
<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-sm font-medium">Workspaces</div>
<div className="text-xs text-muted-foreground">
Show workspace, execution workspace, and work product controls in project and issue UI.
</div>
</div>
<Button
type="button"
variant={workspacesEnabled ? "default" : "outline"}
size="sm"
onClick={() => setWorkspacesEnabled(!workspacesEnabled)}
>
{workspacesEnabled ? "Enabled" : "Disabled"}
</Button>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-muted-foreground" />

View File

@@ -11,7 +11,6 @@ import { useCompany } from "../context/CompanyContext";
import { usePanel } from "../context/PanelContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { queryKeys } from "../lib/queryKeys";
import { useExperimentalWorkspacesEnabled } from "../lib/experimentalSettings";
import { readIssueDetailBreadcrumb } from "../lib/issueDetailBreadcrumb";
import { useProjectOrder } from "../hooks/useProjectOrder";
import { relativeTime, cn, formatTokens, visibleRunCostUsd } from "../lib/utils";
@@ -216,7 +215,6 @@ function ActorIdentity({ evt, agentMap }: { evt: ActivityEvent; agentMap: Map<st
export function IssueDetail() {
const { issueId } = useParams<{ issueId: string }>();
const { selectedCompanyId } = useCompany();
const { enabled: experimentalWorkspacesEnabled } = useExperimentalWorkspacesEnabled();
const { openPanel, closePanel, panelVisible, setPanelVisible } = usePanel();
const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
@@ -662,10 +660,7 @@ export function IssueDetail() {
// Ancestors are returned oldest-first from the server (root at end, immediate parent at start)
const ancestors = issue.ancestors ?? [];
const workProducts = issue.workProducts ?? [];
const showOutputsTab =
experimentalWorkspacesEnabled ||
Boolean(issue.currentExecutionWorkspace) ||
workProducts.length > 0;
const showOutputsTab = Boolean(issue.currentExecutionWorkspace) || workProducts.length > 0;
const handleFilePicked = async (evt: ChangeEvent<HTMLInputElement>) => {
const files = evt.target.files;