Keep new workspace cards consistent after compact upgrades (#5843)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson 2026-06-19 15:34:46 -07:00 committed by GitHub
parent 4f4d520149
commit c442f3aa00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 160 additions and 24 deletions

View File

@ -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']]

View File

@ -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

View File

@ -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')

View File

@ -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(
<WorktreeCard
worktree={makeWorktree({ isPinned: false })}
repo={makeRepo()}
isActive={false}
/>
)
expect(markup).toContain('🦊')
expect(markup).toContain('Project orca')
expect(markup).not.toContain('data-worktree-card-meta-row=""')
})
})

View File

@ -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=""

View File

@ -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<RuntimeRpcResponse<unknown>> {
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<RuntimeRpcResponse<unknown>> {
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<unknown>) => void)[] = []
vi.doMock('./web-runtime-client', () => ({

View File

@ -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<Partial<PersistedUIState>>(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.

View File

@ -36,7 +36,7 @@ export {
TASK_WORKTREE_CARD_PROPERTIES,
getWorktreeCardModeProperties,
getWorktreeCardModeUpdates,
isLegacyDefaultedCompactWorktreeCardProperties,
isDefaultedCompactWorktreeCardProperties,
normalizeWorktreeCardProperties
} from './worktree-card-properties'

View File

@ -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)
})
})

View File

@ -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