diff --git a/docs/compact-worktree-card-setting-graduation.md b/docs/compact-worktree-card-setting-graduation.md new file mode 100644 index 000000000..b69738d35 --- /dev/null +++ b/docs/compact-worktree-card-setting-graduation.md @@ -0,0 +1,94 @@ +# Compact Worktree Card Setting Graduation + +## Problem + +`experimentalCompactWorktreeCards` is exposed in two places: the Experimental settings pane and the +sidebar workspace options menu. The sidebar menu already presents the durable user-facing model as +`Card layout` with `Detailed` and `Compact`, so the Experimental row is redundant and makes the same +preference look like two separate features. + +## Goal + +Graduate the preference out of Experimental. Keep the sidebar workspace options menu as the +canonical control, preserve existing users' saved compact-card choice, and remove the Experimental +pane row/search entry. + +## Non-goals + +- Do not change worktree card compact layout behavior. +- Do not redesign the workspace options menu. +- Do not remove workspace card property or agent activity layout controls. +- Do not change SSH, provider metadata, prompt-cache, conflict, or port behavior. + +## Design + +1. Add `compactWorktreeCards` to `GlobalSettings` and defaults. +2. Preserve backward compatibility by hydrating `compactWorktreeCards` from legacy + `experimentalCompactWorktreeCards` when the new field is absent. +3. Update sidebar/worktree-card reads and writes to use `compactWorktreeCards`. +4. Remove the Experimental pane row and search entry for `Compact worktree cards`. +5. Keep the legacy optional type field only as a read-only migration input. + +## Data Flow + +- Persistence loads settings with defaults. +- If `compactWorktreeCards` is missing, persistence copies the old + `experimentalCompactWorktreeCards` value, falling back to the default. +- The sidebar `Card layout` menu writes `compactWorktreeCards`. +- `WorktreeCard` reads `settings?.compactWorktreeCards === true`. + +## Edge Cases + +- Old profiles with `experimentalCompactWorktreeCards: true` still render compact cards. +- Old profiles with the flag missing still default to detailed cards. +- Search in Settings no longer finds a duplicate Experimental result. +- Compact-card behavior remains unchanged for metadata rows, unread placement, main-worktree + marker, SSH icons, prompt-cache state, conflicts, sparse checkout badges, and inline agents. + +## Test Plan + +- Update default-settings tests for `compactWorktreeCards`. +- Add a persistence migration test for legacy `experimentalCompactWorktreeCards`. +- Update Experimental pane tests to assert the row/search entry is absent. +- Update WorktreeCard tests to set `compactWorktreeCards`. +- Run focused tests for constants, persistence, ExperimentalPane, and WorktreeCard compact behavior. + +## UI Quality Bar + +The Experimental pane should simply omit the redundant row without leaving spacing gaps. The +workspace options menu should continue to show `Card layout` with `Detailed` and `Compact`. + +## Review Screenshots + +1. Experimental pane without `Compact worktree cards`. +2. Sidebar workspace options menu still showing `Card layout`. + +## Rollout + +1. Add the new setting and legacy hydration. +2. Update card/menu consumers. +3. Remove the Experimental pane/search entry. +4. Update focused tests. +5. Validate the two visible settings surfaces. + +## Lightweight Eng Review + +- Scope: kept to one setting graduation; no card layout behavior changes. +- Architecture/data flow: existing settings persistence remains the boundary; renderer consumers read + the new key, persistence bridges legacy profiles. +- Failure modes covered: + - Existing compact users losing the preference: migration copies the legacy flag. + - Duplicate settings search result: Experimental search entry is removed. + - Renderer/main mismatch: `GlobalSettings`, defaults, and consumers are updated together. +- Test coverage required: + - `src/shared/constants.test.ts` for the default. + - `src/main/persistence.test.ts` for legacy hydration. + - `src/renderer/src/components/settings/ExperimentalPane.test.tsx` for removal. + - Existing WorktreeCard tests updated to the new key. +- Performance/blast radius: no new IPC, polling, file watching, or provider work. +- UI quality bar: Experimental pane has no orphan row; sidebar card layout control remains present. +- Required review screenshots: + 1. Experimental pane without the compact-card toggle. + 2. Sidebar workspace options menu with `Card layout`. +- Residual risks: old profiles may keep the legacy key on disk until settings are next saved, but + runtime behavior uses the migrated value. diff --git a/docs/experimental-compact-worktree-cards.md b/docs/experimental-compact-worktree-cards.md deleted file mode 100644 index 3d968346b..000000000 --- a/docs/experimental-compact-worktree-cards.md +++ /dev/null @@ -1,136 +0,0 @@ -# Experimental Compact Worktree Cards - -## Problem - -PR #2843 made compact one-line worktree cards the default in -`src/renderer/src/components/sidebar/WorktreeCard.tsx:183` and -`src/renderer/src/components/sidebar/WorktreeCard.tsx:557`. The behavior is useful for dense -sidebars, but it is visually different enough that it should be trialed behind Experimental first. -Users also cannot infer why some cards remain two-line: the hidden row only disappears when it -would carry no distinct visible metadata. - -## Goal - -Add an Experimental setting, off by default, that controls the compact-card behavior. When disabled, -workspace cards use the pre-compact layout: branch line stays visible, unread remains in the left -status column, PR/issue/comment/port badges stay in the metadata row, and the primary worktree uses -the pre-compact `primary` badge instead of the compact star. When enabled, keep the current compact -behavior from PR #2843. - -## Non-goals - -- Do not redesign worktree card metadata. -- Do not remove card property settings such as PR, ports, issue, comment, unread, or status. -- Do not change SSH, GitLab/GitHub provider lookup, prompt-cache timer, or workspace port logic. -- Do not add per-repo or per-worktree compact preferences. - -## Design - -1. Add `experimentalCompactWorktreeCards: boolean` to `GlobalSettings`, defaulting to `false`. -2. Add an Experimental pane toggle titled `Compact worktree cards`. - Copy must explain the rule: cards collapse only when the second line would be redundant or empty; - real metadata such as a different branch, repo badge, conflict/cache state, or folder badge keeps - the card taller. -3. In `WorktreeCard`, derive `const compactCards = - settings?.experimentalCompactWorktreeCards === true` from the existing settings read and branch - layout: - - Disabled: show branch row for non-folder worktrees, keep unread in the left column, keep - PR/issue/comment/port details in the metadata row, and show the pre-compact `primary` badge. - - Enabled: use PR #2843 compact behavior, including title-row unread, details/ports cluster, and - primary star. -4. Preserve the accessible title tooltip in `WorktreeTitleInlineRename`; it is useful in both modes - and replaces the native `title` attribute. -5. Keep `CacheTimer` presentational. The card can still derive cache state, but the metadata-row gate - must only use compact gating when the experimental flag is enabled. In disabled mode, render the - metadata row even when the cache timer is inactive. - -## Data Flow - -- Main process persistence loads settings via `getDefaultSettings()`. -- Renderer settings pane toggles `experimentalCompactWorktreeCards` through `updateSettings`. -- `WorktreeCard` reads `settings?.experimentalCompactWorktreeCards === true`; missing/null settings - must behave as disabled so existing server-render tests and old profiles do not compact. -- Card layout branches locally; no IPC, SSH, or provider data path changes. - -## Edge Cases - -- Missing legacy setting hydrates to `false`, so existing users get the less-surprising two-line - default after upgrade. -- Folder repos keep their folder badge in the metadata row in both modes. -- Custom display names keep branch metadata in compact mode because the branch differs. -- Repo-grouped cards with `hideRepoBadge` can become one-line only when compact mode is enabled and - no other metadata is visible. -- Active prompt-cache timer and conflict state keep the metadata row visible in compact mode. -- SSH cards keep the SSH icon on the title row in both modes. -- Sparse checkout badges, remote-branch conflict warnings, inline agents, and lineage child chips - are independent rows/badges and must not be hidden by the compact toggle. - -## Test Plan - -- Unit/render tests: - - default settings value is `false`; - - Experimental pane renders the toggle and explanatory copy; - - compact disabled renders a metadata row and branch when title equals branch; - - compact disabled keeps unread in the left status column, details/ports in the metadata row, and - the `primary` badge for the main worktree; - - compact enabled hides the redundant metadata row; - - compact enabled moves unread/details/ports to the title row and uses the primary star; - - compact enabled preserves branch row when title differs from branch; - - title tooltip remains focusable and does not use native `title`. -- Validation: - - Electron screenshot with compact disabled: repeated branch cards are two-line. - - Electron screenshot with compact enabled: repeated branch cards collapse to one-line. - - Electron screenshot of Experimental setting showing explanatory copy. - -## UI Quality Bar - -- Toggle follows existing Experimental pane layout, spacing, typography, and switch style. -- Copy is concise and explains what counts as metadata without teaching implementation details. -- Sidebar cards do not overlap, clip icons, or jump unexpectedly when the setting changes. -- Compact-disabled mode should look understandable rather than like an accidental regression. - -## Review Screenshots - -1. Experimental pane with `Compact worktree cards` toggle off. -2. Sidebar card list with compact disabled: repeated branch cards show the second line. -3. Sidebar card list with compact enabled: repeated branch cards collapse to one line. - -## Rollout - -1. Add setting type/default/search entry/persistence tests. -2. Add Experimental pane UI. -3. Branch `WorktreeCard` layout behind the setting. -4. Update focused render tests. -5. Validate with Electron screenshots. - -## Lightweight Eng Review - -- Scope: Keep the PR to one persisted Experimental flag, one settings row, and local card layout - branching. No new metadata model or card explanation surface. -- Architecture/data flow: Settings persistence already merges defaults with parsed settings; adding - a default-off boolean is enough for old profiles and SSH/web clients because renderer state is - hydrated from the same settings object. -- Failure modes covered: - - Old profiles unexpectedly compacting: default false. - - Toggle copy failing to explain mixed one-line/two-line cards: explicit description. - - Tests only checking branch text instead of row presence: use `data-worktree-card-meta-row`. - - Shipping a half-compact default: assert unread placement, details/ports placement, and - primary badge/star per mode. - - Prompt-cache/conflict metadata disappearing: metadata-row gate keeps these rows in compact mode. -- Test coverage required: - - `src/shared/constants.test.ts` for default value. - - `src/renderer/src/components/settings/ExperimentalPane.test.tsx` or adjacent render test for - settings copy/toggle behavior. - - `src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx` for enabled/disabled - layout behavior. - - Existing `WorktreeTitleInlineRename.test.tsx` remains relevant for tooltip behavior. -- Performance/blast radius: Reuse the existing settings read in `WorktreeCard`; no new polling, IPC, - file watching, or provider calls. -- UI quality bar: Settings row should match existing Experimental rows; sidebar must remain dense - but mixed-height behavior should be explainable from the toggle copy. -- Required review screenshots: - 1. Experimental setting off. - 2. Sidebar compact disabled. - 3. Sidebar compact enabled. -- Residual risks: The mixed card heights are still inherently subtle; this PR explains the rule in - settings but does not add per-card visible reasons. diff --git a/src/main/codex-accounts/runtime-home-service.test.ts b/src/main/codex-accounts/runtime-home-service.test.ts index 99bab08e6..fc0e3720e 100644 --- a/src/main/codex-accounts/runtime-home-service.test.ts +++ b/src/main/codex-accounts/runtime-home-service.test.ts @@ -129,7 +129,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings experimentalPet: false, experimentalActivity: true, experimentalTerminalAttention: false, - experimentalCompactWorktreeCards: false, + compactWorktreeCards: false, experimentalWorktreeSymlinks: false, terminalWindowsShell: 'powershell.exe', terminalWindowsPowerShellImplementation: 'powershell.exe', diff --git a/src/main/codex-accounts/service.test.ts b/src/main/codex-accounts/service.test.ts index 7d17596d6..6914b2ab4 100644 --- a/src/main/codex-accounts/service.test.ts +++ b/src/main/codex-accounts/service.test.ts @@ -133,7 +133,7 @@ function createSettings(overrides: Partial = {}): GlobalSettings experimentalPet: false, experimentalActivity: true, experimentalTerminalAttention: false, - experimentalCompactWorktreeCards: false, + compactWorktreeCards: false, experimentalWorktreeSymlinks: false, terminalWindowsShell: 'powershell.exe', terminalWindowsPowerShellImplementation: 'powershell.exe', diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index 86e652af8..3b3241bf9 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -3448,6 +3448,23 @@ describe('Store', () => { expect(store.getSettings().experimentalPet).toBe(true) }) + it('migrates the legacy experimental compact worktree cards setting', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: { experimentalCompactWorktreeCards: true }, + ui: {}, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {} + }) + + const store = await createStore() + + expect(store.getSettings().compactWorktreeCards).toBe(true) + expect(store.getSettings().experimentalCompactWorktreeCards).toBeUndefined() + }) + it('defaults legacy experimentalActivity profiles off once', async () => { writeDataFile({ schemaVersion: 1, diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 105ec8803..9b36058f3 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -1923,6 +1923,13 @@ export class Store { ...migratedTerminalCursorStyle, experimentalActivity: migratedExperimentalActivity, experimentalActivityDefaultedOffForAllUsers: true, + // Why: compact worktree cards graduated from Experimental; preserve + // the old opt-in for profiles written during the rollout. + compactWorktreeCards: + parsed.settings?.compactWorktreeCards ?? + parsed.settings?.experimentalCompactWorktreeCards ?? + defaults.settings.compactWorktreeCards, + experimentalCompactWorktreeCards: undefined, terminalMacOptionAsAlt: migratedOptionAsAlt, terminalMacOptionAsAltMigrated: true, floatingTerminalEnabled: migratedFloatingTerminalEnabled, diff --git a/src/renderer/src/components/settings/ExperimentalPane.test.tsx b/src/renderer/src/components/settings/ExperimentalPane.test.tsx index 4dcb55d6b..387ec7447 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.test.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.test.tsx @@ -2,7 +2,7 @@ import { renderToStaticMarkup } from 'react-dom/server' import { describe, expect, it, vi } from 'vitest' import { getDefaultSettings } from '../../../../shared/constants' import { ExperimentalPane } from './ExperimentalPane' -import { EXPERIMENTAL_SEARCH_ENTRY } from './experimental-search' +import { EXPERIMENTAL_PANE_SEARCH_ENTRIES } from './experimental-search' vi.mock('../../store', () => ({ useAppStore: (selector: (state: { settingsSearchQuery: string }) => unknown) => @@ -10,15 +10,14 @@ vi.mock('../../store', () => ({ })) describe('ExperimentalPane', () => { - it('renders compact worktree cards as an off-by-default experimental switch', () => { + it('does not render compact worktree cards after graduation from Experimental', () => { const markup = renderToStaticMarkup( ) - expect(markup).toContain('Compact worktree cards') - expect(markup).toContain('aria-checked="false"') - expect(markup).toContain('single title row') - expect(markup).toContain('selected properties on a second row') - expect(EXPERIMENTAL_SEARCH_ENTRY.compactWorktreeCards.keywords).toContain('metadata') + expect(markup).not.toContain('Compact worktree cards') + expect(EXPERIMENTAL_PANE_SEARCH_ENTRIES.map((entry) => entry.title)).not.toContain( + 'Compact worktree cards' + ) }) }) diff --git a/src/renderer/src/components/settings/ExperimentalPane.tsx b/src/renderer/src/components/settings/ExperimentalPane.tsx index 7dde24c0e..9115b9182 100644 --- a/src/renderer/src/components/settings/ExperimentalPane.tsx +++ b/src/renderer/src/components/settings/ExperimentalPane.tsx @@ -27,9 +27,6 @@ export function ExperimentalPane({ const showTerminalAttention = matchesSettingsSearch(searchQuery, [ EXPERIMENTAL_SEARCH_ENTRY.terminalAttention ]) - const showCompactWorktreeCards = matchesSettingsSearch(searchQuery, [ - EXPERIMENTAL_SEARCH_ENTRY.compactWorktreeCards - ]) const showWorktreeSymlinks = matchesSettingsSearch(searchQuery, [ EXPERIMENTAL_SEARCH_ENTRY.symlinks ]) @@ -152,46 +149,6 @@ export function ExperimentalPane({ ) : null} - {showCompactWorktreeCards ? ( - -
-
- -

- Keeps workspace cards to a single title row. The detailed layout restores the - branch, project, cache timer, and selected properties on a second row. -

-
- -
-
- ) : null} - {showWorktreeSymlinks ? ( opt.id === sortBy)?.label ?? 'Sort' const projectOrderLabel = PROJECT_ORDER_OPTIONS.find((opt) => opt.id === projectOrderBy)?.label ?? 'Manual' - const cardLayout = settings?.experimentalCompactWorktreeCards ? 'compact' : 'detailed' + const cardLayout = settings?.compactWorktreeCards ? 'compact' : 'detailed' const cardLayoutLabel = CARD_LAYOUT_OPTIONS.find((opt) => opt.id === cardLayout)?.label ?? 'Detailed' const visiblePropertyCount = PROPERTY_OPTIONS.filter((opt) => @@ -305,7 +305,7 @@ const SidebarWorkspaceOptionsMenu = React.memo(function SidebarWorkspaceOptionsM value={cardLayout} onValueChange={(value) => { void updateSettings({ - experimentalCompactWorktreeCards: value === 'compact' + compactWorktreeCards: value === 'compact' }) }} > diff --git a/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx index eba4c9880..475b82d15 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx @@ -18,7 +18,7 @@ const setWorkspacePortScanRefreshing = vi.fn() let worktreeCardProperties: WorktreeCardProperty[] = ['pr', 'ports'] let hostedReviewCache: Record = {} let workspacePortScan: { key: string; result: WorkspacePortScanResult } | null = null -let settings: Partial | null = { experimentalCompactWorktreeCards: true } +let settings: Partial | null = { compactWorktreeCards: true } vi.mock('@/store', () => ({ useAppStore: (selector: (state: unknown) => unknown) => @@ -152,7 +152,7 @@ describe('WorktreeCard compact hover details', () => { worktreeCardProperties = ['pr', 'ports'] hostedReviewCache = {} workspacePortScan = null - settings = { experimentalCompactWorktreeCards: true } + settings = { compactWorktreeCards: true } }) it('shows PR and live port details from the compact worktree title hover', async () => { diff --git a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx index 9ac290b0e..d7dc10620 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx @@ -218,7 +218,7 @@ describe('WorktreeCard linked PR display', () => { }) it('keeps issue, Linear issue, PR, and notes metadata out of compact cards', async () => { - settings = { experimentalCompactWorktreeCards: true } + settings = { compactWorktreeCards: true } worktreeCardProperties = ['issue', 'linear-issue', 'pr', 'comment'] const { default: WorktreeCard } = await import('./WorktreeCard') diff --git a/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx index a3623fbe3..6dcb03033 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx @@ -224,7 +224,7 @@ describe('WorktreeCard quick actions', () => { it('omits the repeated branch metadata row when compact cards are enabled', () => { worktreeCardProperties = [] - settings = { experimentalCompactWorktreeCards: true } + settings = { compactWorktreeCards: true } const markup = renderToStaticMarkup( { it('omits the branch metadata row when the workspace has a custom title', () => { worktreeCardProperties = [] - settings = { experimentalCompactWorktreeCards: true } + settings = { compactWorktreeCards: true } const markup = renderToStaticMarkup( { it('moves unread and primary into the title row when compact cards are enabled', () => { worktreeCardProperties = ['status', 'unread'] - settings = { experimentalCompactWorktreeCards: true } + settings = { compactWorktreeCards: true } const markup = renderToStaticMarkup( s.fetchIssue) const fetchLinearIssue = useAppStore((s) => s.fetchLinearIssue) const cardProps = useAppStore((s) => s.worktreeCardProperties) - const compactCards = settings?.experimentalCompactWorktreeCards === true + const compactCards = settings?.compactWorktreeCards === true const handleEditIssue = useCallback( (e: React.MouseEvent) => { e.stopPropagation() diff --git a/src/shared/constants.test.ts b/src/shared/constants.test.ts index b7a7b6761..e7b225169 100644 --- a/src/shared/constants.test.ts +++ b/src/shared/constants.test.ts @@ -49,8 +49,8 @@ describe('getDefaultSettings', () => { }) }) - it('keeps compact worktree cards experimental and disabled by default', () => { - expect(getDefaultSettings('/tmp').experimentalCompactWorktreeCards).toBe(false) + it('keeps compact worktree cards disabled by default', () => { + expect(getDefaultSettings('/tmp').compactWorktreeCards).toBe(false) }) }) diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 97ebf48b2..1c95e58e4 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -311,7 +311,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings { experimentalActivity: false, experimentalActivityDefaultedOffForAllUsers: true, experimentalTerminalAttention: false, - experimentalCompactWorktreeCards: false, + compactWorktreeCards: false, experimentalWorktreeSymlinks: false, // Why: local desktop remains the default server until the user explicitly // selects a saved runtime environment. diff --git a/src/shared/types.ts b/src/shared/types.ts index ebb6c2fad..4000751f9 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2297,9 +2297,12 @@ export type GlobalSettings = { * and agent-completion events. Opt-in while the signal/noise balance is * being tested. */ experimentalTerminalAttention: boolean - /** Experimental: compact worktree cards by hiding a redundant metadata row - * when the title and branch already say the same thing. */ - experimentalCompactWorktreeCards: boolean + /** Compact worktree cards by hiding a redundant metadata row when the title + * and branch already say the same thing. */ + compactWorktreeCards: boolean + /** Legacy persisted key from the Experimental rollout. New writes use + * compactWorktreeCards. */ + experimentalCompactWorktreeCards?: boolean /** Experimental: when creating a worktree, automatically symlink a * user-configured set of files/folders from the primary checkout (e.g. * `.env`, `node_modules`) into the new worktree. Opt-in while the