From c442f3aa0056905b1e5a8a74c2da84077387357c Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 19 Jun 2026 15:34:46 -0700 Subject: [PATCH] Keep new workspace cards consistent after compact upgrades (#5843) Co-authored-by: Orca --- src/main/persistence.test.ts | 21 +++++ src/main/persistence.ts | 4 +- .../WorktreeCard.compact-hover.test.tsx | 3 +- .../WorktreeCard.pinned-repo-icon.test.tsx | 18 ++++ .../src/components/sidebar/WorktreeCard.tsx | 11 ++- src/renderer/src/web/web-preload-api.test.ts | 84 +++++++++++++++++++ src/renderer/src/web/web-preload-api.ts | 17 +++- src/shared/constants.ts | 2 +- src/shared/worktree-card-properties.test.ts | 19 ++--- src/shared/worktree-card-properties.ts | 5 +- 10 files changed, 160 insertions(+), 24 deletions(-) diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index 9ac653533..d63f9cf31 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -5277,6 +5277,27 @@ describe('Store', () => { expect(store.getUI().worktreeCardProperties).not.toContain('automation') }) + it('preserves the current defaulted Compact preset without expanding display toggles', async () => { + writeDataFile({ + schemaVersion: 1, + repos: [], + worktreeMeta: {}, + settings: { compactWorktreeCards: true, experimentalNewWorktreeCardStyle: true }, + ui: { + worktreeCardProperties: ['status', 'unread'], + _worktreeCardModeDefaulted: true + }, + githubCache: { pr: {}, issue: {} }, + workspaceSession: {} + }) + const store = await createStore() + + expect(store.getSettings().compactWorktreeCards).toBe(true) + expect(store.getUI().worktreeCardProperties).toEqual(['status', 'unread']) + expect(store.getUI().worktreeCardProperties).not.toContain('ports') + expect(store.getUI().worktreeCardProperties).not.toContain('inline-agents') + }) + it.each([ ['raw', ['status', 'automation']], ['normalized', ['status', 'unread', 'automation']] diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 0492a7b55..c99eb4343 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -90,7 +90,7 @@ import { getDefaultRepoHookSettings, getDefaultWorkspaceSession, getWorktreeCardModeProperties, - isLegacyDefaultedCompactWorktreeCardProperties, + isDefaultedCompactWorktreeCardProperties, normalizeAgentActivityDisplayMode, normalizeWorktreeCardProperties, ONBOARDING_FLOW_VERSION, @@ -2806,7 +2806,7 @@ export class Store { const needsLegacyDefaultedCompactMigration = loadedCompactWorktreeCards && parsed.ui?._worktreeCardModeDefaulted === true && - isLegacyDefaultedCompactWorktreeCardProperties(rawCardProps) + isDefaultedCompactWorktreeCardProperties(rawCardProps) const migratedCardProps = (() => { if (!Array.isArray(rawCardProps)) { return undefined 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 3d7e25d73..91be2c249 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx @@ -365,7 +365,8 @@ describe('WorktreeCard compact hover details', () => { /> ) - expect(markup).toContain('data-worktree-card-meta-row=""') + expect(markup).toContain('Workspace metadata') + expect(markup).not.toContain('data-worktree-card-meta-row=""') expectParentBodyIsHoverTrigger(markup) expect(markup.match(/data-hover-open-delay="100"/g)).toHaveLength(1) expect(markup).toContain('Reviewer handoff note') diff --git a/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx index cb6128d1d..cd01ae925 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx @@ -142,4 +142,22 @@ describe('WorktreeCard pinned repo icon', () => { expect(markup).not.toContain('🦊') expect(markup).not.toContain('Project orca') }) + + it('uses the pinned-style repo icon in new card style instead of a metadata-row badge', async () => { + settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true } + worktreeCardProperties = ['status'] + const { default: WorktreeCard } = await import('./WorktreeCard') + + const markup = renderToStaticMarkup( + + ) + + expect(markup).toContain('🦊') + expect(markup).toContain('Project orca') + expect(markup).not.toContain('data-worktree-card-meta-row=""') + }) }) diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index e8bc28686..99e2f10c8 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -220,8 +220,8 @@ const WorktreeCard = React.memo(function WorktreeCard({ const fetchIssue = useAppStore((s) => s.fetchIssue) const fetchLinearIssue = useAppStore((s) => s.fetchLinearIssue) const cardProps = useAppStore((s) => s.worktreeCardProperties) - const compactCards = settings?.compactWorktreeCards === true const newCardStyle = settings?.experimentalNewWorktreeCardStyle === true + const compactCards = !newCardStyle && settings?.compactWorktreeCards === true const activeSurfaceIsSecondary = isActiveSurface && activeSurfaceVariant === 'secondary' const handleEditIssue = useCallback( (e: React.MouseEvent) => { @@ -1031,9 +1031,13 @@ const WorktreeCard = React.memo(function WorktreeCard({ // Why: pinned trees mix repos in one section; a leading repo icon keeps the // list scannable, so it shows regardless of groupBy's hideRepoBadge. const showPinnedRepoIcon = inPinnedSection && !!repo + // Why: the new card style retired the Compact/Detailed layout switch; repo + // identity uses the same compact chip as pinned cards instead of a lower pill. + const showRepoIdentityInTitle = newCardStyle || compactCards const showInlineRepoBadge = - compactCards && !!repo && !hideRepoBadge && !isFolder && !showPinnedRepoIcon - const showRepoBadgeInMetaRow = !compactCards && !!repo && !hideRepoBadge && !showPinnedRepoIcon + showRepoIdentityInTitle && !!repo && !hideRepoBadge && !isFolder && !showPinnedRepoIcon + const showRepoBadgeInMetaRow = + !showRepoIdentityInTitle && !!repo && !hideRepoBadge && !showPinnedRepoIcon const showHostContextBadge = !compactCards && !!hostContextLabel const showDetachedHeadInMetaRow = !compactCards && !isFolder && detachedHeadDisplay !== null const showBranch = @@ -1207,7 +1211,6 @@ const WorktreeCard = React.memo(function WorktreeCard({ className={cn( 'flex shrink-0 justify-center', newCardStyle ? 'mr-1 w-5 items-center' : 'items-start pt-[2px]', - compactCards && newCardStyle && 'items-center pt-0', affiliateListMode && 'px-1' )} data-worktree-card-status-slot="" diff --git a/src/renderer/src/web/web-preload-api.test.ts b/src/renderer/src/web/web-preload-api.test.ts index ec236c402..6cdb12921 100644 --- a/src/renderer/src/web/web-preload-api.test.ts +++ b/src/renderer/src/web/web-preload-api.test.ts @@ -762,6 +762,90 @@ describe('web UI preload API', () => { expect(ui.rightSidebarOpen).toBe(true) }) + it('seeds missing local card display properties from runtime-backed compact settings when ui.get is unavailable', async () => { + const runtimeCalls: { method: string; params: unknown }[] = [] + vi.doMock('./web-runtime-client', () => ({ + WebRuntimeClient: class { + call(method: string, params?: unknown): Promise> { + runtimeCalls.push({ method, params }) + if (method === 'settings.get') { + return Promise.resolve({ + id: `call-${runtimeCalls.length}`, + ok: true, + result: { settings: { compactWorktreeCards: true } }, + _meta: { runtimeId: 'runtime-1' } + }) + } + return Promise.resolve({ + id: `call-${runtimeCalls.length}`, + ok: false, + error: { code: 'method_not_found', message: 'Unknown method' }, + _meta: { runtimeId: 'runtime-1' } + }) + } + + close(): void {} + } + })) + + const globals = installBrowserGlobals('Linux') + writeStoredRuntimeEnvironment(globals.storage) + const { installWebPreloadApi } = await import('./web-preload-api') + installWebPreloadApi() + + await globals.window.api.settings.get() + const ui = await globals.window.api.ui.get() + + expect(ui.worktreeCardProperties).toEqual(['status', 'unread']) + expect(ui.worktreeCardProperties).not.toContain('ports') + expect(ui.worktreeCardProperties).not.toContain('inline-agents') + expect(runtimeCalls.map((call) => call.method)).toEqual(['settings.get', 'ui.get']) + }) + + it('preserves explicit local card display properties when compact fallback settings are present', async () => { + const runtimeCalls: { method: string; params: unknown }[] = [] + vi.doMock('./web-runtime-client', () => ({ + WebRuntimeClient: class { + call(method: string, params?: unknown): Promise> { + runtimeCalls.push({ method, params }) + if (method === 'settings.get') { + return Promise.resolve({ + id: `call-${runtimeCalls.length}`, + ok: true, + result: { settings: { compactWorktreeCards: true } }, + _meta: { runtimeId: 'runtime-1' } + }) + } + return Promise.resolve({ + id: `call-${runtimeCalls.length}`, + ok: false, + error: { code: 'method_not_found', message: 'Unknown method' }, + _meta: { runtimeId: 'runtime-1' } + }) + } + + close(): void {} + } + })) + + const globals = installBrowserGlobals('Linux') + writeStoredRuntimeEnvironment(globals.storage) + globals.storage.setItem( + 'orca.web.ui.v1', + JSON.stringify({ worktreeCardProperties: ['status', 'pr'] }) + ) + const { installWebPreloadApi } = await import('./web-preload-api') + installWebPreloadApi() + + await globals.window.api.settings.get() + const ui = await globals.window.api.ui.get() + + expect(ui.worktreeCardProperties).toEqual(['status', 'unread', 'pr']) + expect(ui.worktreeCardProperties).not.toContain('ports') + expect(ui.worktreeCardProperties).not.toContain('inline-agents') + expect(runtimeCalls.map((call) => call.method)).toEqual(['settings.get', 'ui.get']) + }) + it('keeps newer feature interaction counts when runtime responses resolve out of order', async () => { const pending: ((response: RuntimeRpcResponse) => void)[] = [] vi.doMock('./web-runtime-client', () => ({ diff --git a/src/renderer/src/web/web-preload-api.ts b/src/renderer/src/web/web-preload-api.ts index 6d4ec2f45..83e3d9993 100644 --- a/src/renderer/src/web/web-preload-api.ts +++ b/src/renderer/src/web/web-preload-api.ts @@ -31,6 +31,7 @@ import { getDefaultSettings, getDefaultUIState, getDefaultWorkspaceSession, + getWorktreeCardModeProperties, normalizeAgentActivityDisplayMode, normalizeWorktreeCardProperties, ONBOARDING_FLOW_VERSION @@ -2758,11 +2759,19 @@ function closeWebOnboarding(base: OnboardingState): OnboardingState { function readLocalWebUIState(): PersistedUIState { const defaults = getDefaultUIState() const stored = readJson>(UI_STORAGE_KEY, {}) - if (typeof stored.rightSidebarOpen === 'boolean') { - return mergeWebUIState(defaults, stored) - } const storedSettings = getStoredSettings() - return mergeWebUIState(defaults, { + const base = { + ...defaults, + // Why: when runtime ui.get is unavailable, web fallback must mirror the + // main-process missing-property seed from the legacy card layout mode. + worktreeCardProperties: getWorktreeCardModeProperties( + storedSettings.compactWorktreeCards ? 'Compact' : 'Default' + ) + } + if (typeof stored.rightSidebarOpen === 'boolean') { + return mergeWebUIState(base, stored) + } + return mergeWebUIState(base, { ...stored, // Why: web fallback lacks main-process normalization, so migrate the // retired setting only when the local UI preference is still absent. diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 00da56400..b502d4cc7 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -36,7 +36,7 @@ export { TASK_WORKTREE_CARD_PROPERTIES, getWorktreeCardModeProperties, getWorktreeCardModeUpdates, - isLegacyDefaultedCompactWorktreeCardProperties, + isDefaultedCompactWorktreeCardProperties, normalizeWorktreeCardProperties } from './worktree-card-properties' diff --git a/src/shared/worktree-card-properties.test.ts b/src/shared/worktree-card-properties.test.ts index 00a738380..6a24606c1 100644 --- a/src/shared/worktree-card-properties.test.ts +++ b/src/shared/worktree-card-properties.test.ts @@ -5,7 +5,7 @@ import { TASK_WORKTREE_CARD_PROPERTIES, getWorktreeCardModeProperties, getWorktreeCardModeUpdates, - isLegacyDefaultedCompactWorktreeCardProperties, + isDefaultedCompactWorktreeCardProperties, normalizeWorktreeCardProperties } from './worktree-card-properties' @@ -62,15 +62,12 @@ describe('worktree card properties', () => { }) }) - it('recognizes only the exact old defaulted Compact preset', () => { - expect(isLegacyDefaultedCompactWorktreeCardProperties(['status', 'automation'])).toBe(true) - expect(isLegacyDefaultedCompactWorktreeCardProperties(['status', 'unread', 'automation'])).toBe( - true - ) - expect(isLegacyDefaultedCompactWorktreeCardProperties(['automation', 'status'])).toBe(false) - expect(isLegacyDefaultedCompactWorktreeCardProperties(['status'])).toBe(false) - expect(isLegacyDefaultedCompactWorktreeCardProperties(['status', 'automation', 'pr'])).toBe( - false - ) + it('recognizes only exact defaulted Compact presets', () => { + expect(isDefaultedCompactWorktreeCardProperties(['status'])).toBe(true) + expect(isDefaultedCompactWorktreeCardProperties(['status', 'unread'])).toBe(true) + expect(isDefaultedCompactWorktreeCardProperties(['status', 'automation'])).toBe(true) + expect(isDefaultedCompactWorktreeCardProperties(['status', 'unread', 'automation'])).toBe(true) + expect(isDefaultedCompactWorktreeCardProperties(['automation', 'status'])).toBe(false) + expect(isDefaultedCompactWorktreeCardProperties(['status', 'automation', 'pr'])).toBe(false) }) }) diff --git a/src/shared/worktree-card-properties.ts b/src/shared/worktree-card-properties.ts index ee1cee312..76b1c32eb 100644 --- a/src/shared/worktree-card-properties.ts +++ b/src/shared/worktree-card-properties.ts @@ -25,6 +25,7 @@ export const DEFAULT_WORKTREE_CARD_PROPERTIES: WorktreeCardProperty[] = [ // Why: compact cards default to the quiet preset; metadata icons remain opt-in // through Show properties instead of appearing automatically. export const COMPACT_WORKTREE_CARD_PROPERTIES: WorktreeCardProperty[] = ['status'] +const NORMALIZED_COMPACT_WORKTREE_CARD_PROPERTIES: WorktreeCardProperty[] = ['status', 'unread'] const LEGACY_COMPACT_WORKTREE_CARD_PROPERTIES_WITH_AUTOMATION: WorktreeCardProperty[] = [ 'status', @@ -83,10 +84,12 @@ export function getWorktreeCardModeUpdates(mode: WorktreeCardMode): { } } -export function isLegacyDefaultedCompactWorktreeCardProperties( +export function isDefaultedCompactWorktreeCardProperties( properties: readonly unknown[] | null | undefined ): boolean { return ( + matchesWorktreeCardProperties(properties, COMPACT_WORKTREE_CARD_PROPERTIES) || + matchesWorktreeCardProperties(properties, NORMALIZED_COMPACT_WORKTREE_CARD_PROPERTIES) || matchesWorktreeCardProperties( properties, LEGACY_COMPACT_WORKTREE_CARD_PROPERTIES_WITH_AUTOMATION