Keep new workspace cards consistent after compact upgrades (#5843)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
4f4d520149
commit
c442f3aa00
|
|
@ -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']]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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=""')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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', () => ({
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export {
|
|||
TASK_WORKTREE_CARD_PROPERTIES,
|
||||
getWorktreeCardModeProperties,
|
||||
getWorktreeCardModeUpdates,
|
||||
isLegacyDefaultedCompactWorktreeCardProperties,
|
||||
isDefaultedCompactWorktreeCardProperties,
|
||||
normalizeWorktreeCardProperties
|
||||
} from './worktree-card-properties'
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue