From b1b7a9dff637e24aeffe5665b45eabb929134b0c Mon Sep 17 00:00:00 2001 From: scotttong Date: Wed, 5 Aug 2026 17:08:09 -0700 Subject: [PATCH] feat(settings): alphabetize experimental cards and drop the Experimental chip (#10924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The Instance Settings area exposes an Experimental page that lists opt-in feature toggles as cards. > - New experimental features were appended to the list over time, so the cards sat in insertion order with no predictable arrangement. > - An unordered list is hard to scan when you are looking for one specific feature. > - Each card also carried a small "Experimental" secondary badge, which is redundant on a page that is itself titled Experimental. > - This pull request sorts every card alphabetically by its title and removes that redundant badge. > - The benefit is a list that is faster to scan and headings that are less cluttered. ## Linked Issues or Issue Description No public GitHub issue exists for this change, so the enhancement is described inline following `.github/ISSUE_TEMPLATE/enhancement.yml`: **What existing behavior does this improve?** The Instance Settings → Experimental page, which lists opt-in feature toggles as a stack of cards. **Subsystem affected** UI — the Instance Experimental settings page (`ui/src/pages/InstanceExperimentalSettings.tsx`). **Current behavior** Cards render in insertion order (the order features happened to be added), so finding a specific feature means scanning the whole list. Several headings also carry a redundant "Experimental" secondary badge. **Proposed behavior** Cards render top-to-bottom in A→Z order by title, and no card shows an "Experimental" secondary badge. Toggle logic, footnotes, conditional visibility, and the "Managed by Paperclip Cloud" badge are unchanged. **Reason and benefit** Alphabetical order makes the list predictable and quick to scan for a specific feature. The "Experimental" badge repeats information already conveyed by the page title, so removing it declutters the headings. **Breaking changes** None. This touches card render order and the removal of a decorative badge only — no state, persistence, toggle, or visibility logic changes. ## What Changed - Sorted every card on the Instance Experimental settings page alphabetically by its heading title. - Removed the redundant "Experimental" secondary badge from the card headings (previously on Apps, Cases, and Chat-Style Tasks). - Added tests asserting the cards render in case-insensitive alphabetical order and that no card renders an "Experimental" secondary badge. - No behavior change: toggle handlers, footnotes, managed-key handling, and conditional cards (Conference Room Chat, worktree-scoped run) are untouched and now sort into their alphabetical slots. ## Verification - `pnpm check:token-gates` → all 3 gates CLEAN. - `npx vitest run ui/src/pages/InstanceExperimentalSettings.test.tsx` → 32/32 tests pass (the suite renders the real component and now covers ordering + badge removal). - `pnpm --filter @paperclipai/ui typecheck` (`tsc -b`) → clean. - Manual: open Instance Settings → Experimental. The cards read A→Z and no card shows an "Experimental" chip. ## Risks Low risk. The change is limited to one page component: card render order and the removal of a decorative badge, plus new tests. No state, persistence, toggle, or visibility logic is modified. ## Model Used Claude Opus 4.8 (Anthropic), model id `claude-opus-4-8`, extended thinking enabled, tool use enabled. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Claude Opus 4.8 --- .../InstanceExperimentalSettings.test.tsx | 64 +++ ui/src/pages/InstanceExperimentalSettings.tsx | 516 +++++++++--------- 2 files changed, 319 insertions(+), 261 deletions(-) diff --git a/ui/src/pages/InstanceExperimentalSettings.test.tsx b/ui/src/pages/InstanceExperimentalSettings.test.tsx index 1cfa6da65e..3e6ae06160 100644 --- a/ui/src/pages/InstanceExperimentalSettings.test.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.test.tsx @@ -862,3 +862,67 @@ describe("InstanceExperimentalSettings — cloud-managed keys", () => { expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenCalledWith({ enableApps: true }); }); }); + +describe("InstanceExperimentalSettings — card ordering and headings (PAP-393)", () => { + let container: HTMLDivElement; + let root: Root | null = null; + + async function renderPage(settings: InstanceExperimentalSettingsWithManaged) { + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ ...settings }); + root = createRoot(container); + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + flushSync(() => { + root!.render( + + + , + ); + }); + await flushReact(); + } + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + mockInstanceSettingsApi.updateExperimental.mockImplementation(async (patch) => ({ + ...defaultExperimentalSettings(), + ...patch, + })); + }); + + afterEach(() => { + flushSync(() => { + root?.unmount(); + }); + root = null; + container.remove(); + vi.clearAllMocks(); + }); + + it("renders every card heading in alphabetical order", async () => { + await renderPage(defaultExperimentalSettings()); + + const headings = [...container.querySelectorAll("h2")].map( + (heading) => heading.textContent ?? "", + ); + + // Sanity: the page rendered a meaningful set of cards, not an empty list. + expect(headings.length).toBeGreaterThan(10); + + const alphabetical = [...headings].sort((a, b) => + a.localeCompare(b, undefined, { sensitivity: "base" }), + ); + expect(headings).toEqual(alphabetical); + }); + + it("no longer renders an 'Experimental' secondary badge on any card", async () => { + await renderPage(defaultExperimentalSettings()); + + const badges = [...container.querySelectorAll('[data-slot="badge"]')].map( + (badge) => badge.textContent?.trim(), + ); + expect(badges).not.toContain("Experimental"); + }); +}); diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index d450863600..40a2a40359 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -87,7 +87,6 @@ function ManagedByCloudBadge() { function ExperimentalToggleCard({ title, - experimental = false, description, footnote, checked, @@ -97,7 +96,6 @@ function ExperimentalToggleCard({ ariaLabel, }: { title: string; - experimental?: boolean; description: string; footnote?: string; checked: boolean; @@ -113,7 +111,6 @@ function ExperimentalToggleCard({

{title}

- {experimental ? Experimental : null} {isManaged ? : null}

{description}

@@ -460,66 +457,8 @@ export function InstanceExperimentalSettings() {
)} - {inWorktree ? ( - -
-
-
-
-

Run tasks in this worktree

- {worktreeRunExecutionManaged ? : null} -
-

- This is an isolated git-worktree preview instance. Turn this on to let the scheduler execute runs - here. Only tasks created after enabling will run automatically — copied/pre-existing tasks stay - parked. Toggling off and on resets the cutoff. -

-
- { - if (worktreeRunExecutionManaged) return; - toggleMutation.mutate({ enableWorktreeRunExecution: checked }); - }} - disabled={toggleMutation.isPending || worktreeRunExecutionManaged} - aria-label="Toggle worktree run execution setting" - /> -
- - {worktreeRunExecutionState.kind === "armed" ? ( -
- - - Running tasks created after{" "} - - {formatActivationTimestamp(worktreeRunExecutionState.activatedAt)} - - . - -
- ) : null} - - {worktreeRunExecutionState.kind === "fail_closed" ? ( -
- -
-

Execution is suppressed — effectively off.

-

- {worktreeRunExecutionState.reason === "instance_mismatch" - ? "This setting was armed in a different instance and copied here, so no tasks run automatically." - : "This setting is missing its activation cutoff, so no tasks run automatically."}{" "} - Toggle it off and back on to arm execution for tasks created here. -

-
-
- ) : null} -
-
- ) : null} - toggleMutation.mutate({ enableApps: checked })} @@ -528,206 +467,6 @@ export function InstanceExperimentalSettings() { ariaLabel="Toggle apps experimental setting" /> - toggleMutation.mutate({ enableCases: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableCases} - ariaLabel="Toggle cases experimental setting" - /> - - toggleMutation.mutate({ enableEnvironments: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableEnvironments} - ariaLabel="Toggle environments experimental setting" - /> - - toggleMutation.mutate({ enableBuiltInAgents: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableBuiltInAgents} - ariaLabel="Toggle built-in agents experimental setting" - /> - - toggleMutation.mutate({ enableBetaSkills: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableBetaSkills} - ariaLabel="Toggle beta skills experimental setting" - /> - - - toggleMutation.mutate( - checked || !enableStatusCards - ? { enableSummaries: checked } - : { enableSummaries: false, enableStatusCards: false }, - ) - } - disabled={toggleMutation.isPending || summariesRequiredByManagedStatusCards} - managed={managedKeys.enableSummaries} - ariaLabel="Toggle summaries experimental setting" - /> - - toggleMutation.mutate({ enableExperimentalFileViewer: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableExperimentalFileViewer} - ariaLabel="Toggle experimental file viewer setting" - /> - - - toggleMutation.mutate( - checked - ? { enableSummaries: true, enableStatusCards: true } - : { enableStatusCards: false }, - ) - } - disabled={toggleMutation.isPending || statusCardsBlockedByManagedSummaries} - managed={managedKeys.enableStatusCards} - ariaLabel="Toggle status cards experimental setting" - /> - - toggleMutation.mutate({ enableExternalObjects: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableExternalObjects} - ariaLabel="Toggle external objects experimental setting" - /> - - toggleMutation.mutate({ enableDecisions: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableDecisions} - ariaLabel="Toggle decisions experimental setting" - /> - - toggleMutation.mutate({ enableGoalsSidebarLink: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableGoalsSidebarLink} - ariaLabel="Toggle goals sidebar link experimental setting" - /> - - toggleMutation.mutate({ enableIsolatedWorkspaces: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableIsolatedWorkspaces} - ariaLabel="Toggle isolated workspaces experimental setting" - /> - - {SHOW_CONFERENCE_ROOM_EXPERIMENTAL_SETTING ? ( - toggleMutation.mutate({ enableConferenceRoomChat: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableConferenceRoomChat} - ariaLabel="Toggle conference room chat experimental setting" - /> - ) : null} - - toggleMutation.mutate({ enableIssuePlanDecompositions: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableIssuePlanDecompositions} - ariaLabel="Toggle task plan decomposition panel experimental setting" - /> - - toggleMutation.mutate({ enableTaskChatRedesign: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableTaskChatRedesign} - ariaLabel="Toggle chat-style tasks experimental setting" - /> - - toggleMutation.mutate({ enableTaskWatchdogs: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableTaskWatchdogs} - ariaLabel="Toggle task watchdogs experimental setting" - /> - - toggleMutation.mutate({ enableServerInfoDebugView: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableServerInfoDebugView} - ariaLabel="Toggle server info debug view experimental setting" - /> - - toggleMutation.mutate({ enableSmokeLab: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.enableSmokeLab} - ariaLabel="Toggle smoke lab experimental setting" - /> - - toggleMutation.mutate({ autoRestartDevServerWhenIdle: checked })} - disabled={toggleMutation.isPending} - managed={managedKeys.autoRestartDevServerWhenIdle} - ariaLabel="Toggle guarded dev-server auto-restart" - /> -
@@ -818,6 +557,261 @@ export function InstanceExperimentalSettings() {
+ toggleMutation.mutate({ autoRestartDevServerWhenIdle: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.autoRestartDevServerWhenIdle} + ariaLabel="Toggle guarded dev-server auto-restart" + /> + + toggleMutation.mutate({ enableBetaSkills: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableBetaSkills} + ariaLabel="Toggle beta skills experimental setting" + /> + + toggleMutation.mutate({ enableBuiltInAgents: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableBuiltInAgents} + ariaLabel="Toggle built-in agents experimental setting" + /> + + toggleMutation.mutate({ enableCases: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableCases} + ariaLabel="Toggle cases experimental setting" + /> + + toggleMutation.mutate({ enableTaskChatRedesign: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableTaskChatRedesign} + ariaLabel="Toggle chat-style tasks experimental setting" + /> + + {SHOW_CONFERENCE_ROOM_EXPERIMENTAL_SETTING ? ( + toggleMutation.mutate({ enableConferenceRoomChat: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableConferenceRoomChat} + ariaLabel="Toggle conference room chat experimental setting" + /> + ) : null} + + toggleMutation.mutate({ enableDecisions: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableDecisions} + ariaLabel="Toggle decisions experimental setting" + /> + + toggleMutation.mutate({ enableEnvironments: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableEnvironments} + ariaLabel="Toggle environments experimental setting" + /> + + toggleMutation.mutate({ enableExternalObjects: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableExternalObjects} + ariaLabel="Toggle external objects experimental setting" + /> + + toggleMutation.mutate({ enableIsolatedWorkspaces: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableIsolatedWorkspaces} + ariaLabel="Toggle isolated workspaces experimental setting" + /> + + toggleMutation.mutate({ enableExperimentalFileViewer: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableExperimentalFileViewer} + ariaLabel="Toggle experimental file viewer setting" + /> + + toggleMutation.mutate({ enableGoalsSidebarLink: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableGoalsSidebarLink} + ariaLabel="Toggle goals sidebar link experimental setting" + /> + + {inWorktree ? ( + +
+
+
+
+

Run tasks in this worktree

+ {worktreeRunExecutionManaged ? : null} +
+

+ This is an isolated git-worktree preview instance. Turn this on to let the scheduler execute runs + here. Only tasks created after enabling will run automatically — copied/pre-existing tasks stay + parked. Toggling off and on resets the cutoff. +

+
+ { + if (worktreeRunExecutionManaged) return; + toggleMutation.mutate({ enableWorktreeRunExecution: checked }); + }} + disabled={toggleMutation.isPending || worktreeRunExecutionManaged} + aria-label="Toggle worktree run execution setting" + /> +
+ + {worktreeRunExecutionState.kind === "armed" ? ( +
+ + + Running tasks created after{" "} + + {formatActivationTimestamp(worktreeRunExecutionState.activatedAt)} + + . + +
+ ) : null} + + {worktreeRunExecutionState.kind === "fail_closed" ? ( +
+ +
+

Execution is suppressed — effectively off.

+

+ {worktreeRunExecutionState.reason === "instance_mismatch" + ? "This setting was armed in a different instance and copied here, so no tasks run automatically." + : "This setting is missing its activation cutoff, so no tasks run automatically."}{" "} + Toggle it off and back on to arm execution for tasks created here. +

+
+
+ ) : null} +
+
+ ) : null} + + toggleMutation.mutate({ enableServerInfoDebugView: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableServerInfoDebugView} + ariaLabel="Toggle server info debug view experimental setting" + /> + + toggleMutation.mutate({ enableSmokeLab: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableSmokeLab} + ariaLabel="Toggle smoke lab experimental setting" + /> + + + toggleMutation.mutate( + checked + ? { enableSummaries: true, enableStatusCards: true } + : { enableStatusCards: false }, + ) + } + disabled={toggleMutation.isPending || statusCardsBlockedByManagedSummaries} + managed={managedKeys.enableStatusCards} + ariaLabel="Toggle status cards experimental setting" + /> + + + toggleMutation.mutate( + checked || !enableStatusCards + ? { enableSummaries: checked } + : { enableSummaries: false, enableStatusCards: false }, + ) + } + disabled={toggleMutation.isPending || summariesRequiredByManagedStatusCards} + managed={managedKeys.enableSummaries} + ariaLabel="Toggle summaries experimental setting" + /> + + toggleMutation.mutate({ enableIssuePlanDecompositions: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableIssuePlanDecompositions} + ariaLabel="Toggle task plan decomposition panel experimental setting" + /> + + toggleMutation.mutate({ enableTaskWatchdogs: checked })} + disabled={toggleMutation.isPending} + managed={managedKeys.enableTaskWatchdogs} + ariaLabel="Toggle task watchdogs experimental setting" + /> + {previewDialogOpen && !autoRecoveryManaged ? (