mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 00:02:19 +00:00
feat(sidebar): let the host pill be turned off per card (#24299)
This commit is contained in:
@@ -81,7 +81,8 @@ describe('Store', () => {
|
||||
'pr',
|
||||
'comment',
|
||||
'ports',
|
||||
'inline-agents'
|
||||
'inline-agents',
|
||||
'host'
|
||||
])
|
||||
expect(store.getUI()._inlineAgentsDefaultedForAllUsers).toBe(true)
|
||||
expect(store.getUI()._expandedWorktreeCardPropertiesDefaulted).toBe(true)
|
||||
@@ -118,7 +119,8 @@ describe('Store', () => {
|
||||
'pr',
|
||||
'comment',
|
||||
'ports',
|
||||
'inline-agents'
|
||||
'inline-agents',
|
||||
'host'
|
||||
])
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('branch')
|
||||
expect(store.getUI()._inlineAgentsDefaultedForAllUsers).toBe(true)
|
||||
@@ -148,7 +150,8 @@ describe('Store', () => {
|
||||
'cli',
|
||||
'comment',
|
||||
'ports',
|
||||
'inline-agents'
|
||||
'inline-agents',
|
||||
'host'
|
||||
])
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('branch')
|
||||
expect(store.getUI()._worktreeCardModeDefaulted).toBe(true)
|
||||
@@ -178,7 +181,8 @@ describe('Store', () => {
|
||||
'jira-issue',
|
||||
'pr',
|
||||
'ports',
|
||||
'inline-agents'
|
||||
'inline-agents',
|
||||
'host'
|
||||
])
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('branch')
|
||||
})
|
||||
@@ -200,7 +204,7 @@ describe('Store', () => {
|
||||
})
|
||||
const store = await createStore()
|
||||
|
||||
expect(store.getUI().worktreeCardProperties).toEqual(['status', 'unread', 'pr'])
|
||||
expect(store.getUI().worktreeCardProperties).toEqual(['status', 'unread', 'pr', 'host'])
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('branch')
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('ports')
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('inline-agents')
|
||||
@@ -268,7 +272,8 @@ describe('Store', () => {
|
||||
'linear-issue',
|
||||
'pr',
|
||||
'comment',
|
||||
'ports'
|
||||
'ports',
|
||||
'host'
|
||||
])
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('branch')
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('inline-agents')
|
||||
@@ -306,7 +311,8 @@ describe('Store', () => {
|
||||
'jira-issue',
|
||||
'pr',
|
||||
'ports',
|
||||
'inline-agents'
|
||||
'inline-agents',
|
||||
'host'
|
||||
])
|
||||
expect(
|
||||
store.getUI().worktreeCardProperties?.filter((property) => property === 'jira-issue')
|
||||
@@ -336,7 +342,8 @@ describe('Store', () => {
|
||||
'unread',
|
||||
'issue',
|
||||
'linear-issue',
|
||||
'pr'
|
||||
'pr',
|
||||
'host'
|
||||
])
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('jira-issue')
|
||||
})
|
||||
@@ -413,6 +420,51 @@ describe('Store', () => {
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('inline-agents')
|
||||
})
|
||||
|
||||
it('backfills host once for profiles stamped before it became a card property', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
worktreeMeta: {},
|
||||
settings: { compactWorktreeCards: false },
|
||||
ui: {
|
||||
worktreeCardProperties: ['status', 'unread', 'pr', 'ports', 'inline-agents'],
|
||||
_inlineAgentsDefaultedForAllUsers: true,
|
||||
_expandedWorktreeCardPropertiesDefaulted: true,
|
||||
_jiraIssueWorktreeCardPropertyDefaulted: true
|
||||
},
|
||||
githubCache: { pr: {}, issue: {} },
|
||||
workspaceSession: {}
|
||||
})
|
||||
const store = await createStore()
|
||||
|
||||
expect(
|
||||
store.getUI().worktreeCardProperties?.filter((property) => property === 'host')
|
||||
).toHaveLength(1)
|
||||
expect(store.getUI()._hostWorktreeCardPropertyDefaulted).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves a deliberate host removal after the backfill has run', async () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
worktreeMeta: {},
|
||||
settings: { compactWorktreeCards: false },
|
||||
ui: {
|
||||
worktreeCardProperties: ['status', 'unread', 'pr'],
|
||||
_inlineAgentsDefaultedForAllUsers: true,
|
||||
_expandedWorktreeCardPropertiesDefaulted: true,
|
||||
_jiraIssueWorktreeCardPropertyDefaulted: true,
|
||||
_hostWorktreeCardPropertyDefaulted: true
|
||||
},
|
||||
githubCache: { pr: {}, issue: {} },
|
||||
workspaceSession: {}
|
||||
})
|
||||
const store = await createStore()
|
||||
|
||||
expect(store.getUI().worktreeCardProperties).toEqual(['status', 'unread', 'pr'])
|
||||
expect(store.getUI().worktreeCardProperties).not.toContain('host')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['raw', ['status', 'automation']],
|
||||
['normalized', ['status', 'unread', 'automation']]
|
||||
|
||||
@@ -78,6 +78,7 @@ export function normalizeLoadedUiState(
|
||||
const inlineAgentsMigrated = parsed.ui?._inlineAgentsDefaultedForAllUsers === true
|
||||
const expandedCardPropsMigrated = parsed.ui?._expandedWorktreeCardPropertiesDefaulted === true
|
||||
const jiraIssueCardPropDefaulted = parsed.ui?._jiraIssueWorktreeCardPropertyDefaulted === true
|
||||
const hostCardPropDefaulted = parsed.ui?._hostWorktreeCardPropertyDefaulted === true
|
||||
const hadExperimentOn = readDeprecatedExperimentFlag(parsed)
|
||||
const deliberateUncheck =
|
||||
hadExperimentOn && Array.isArray(rawCardProps) && !rawCardProps.includes('inline-agents')
|
||||
@@ -119,7 +120,12 @@ export function normalizeLoadedUiState(
|
||||
jiraIssueCardPropDefaulted || expandedCandidate.includes('jira-issue')
|
||||
? expandedCandidate
|
||||
: [...expandedCandidate, 'jira-issue' as const]
|
||||
const normalized = normalizeWorktreeCardProperties(jiraCandidate)
|
||||
// Why: the host pill was unconditional before it became a property, so existing profiles get it back once rather than silently losing it.
|
||||
const hostCandidate =
|
||||
hostCardPropDefaulted || jiraCandidate.includes('host')
|
||||
? jiraCandidate
|
||||
: [...jiraCandidate, 'host' as const]
|
||||
const normalized = normalizeWorktreeCardProperties(hostCandidate)
|
||||
const changed =
|
||||
normalized.length !== rawCardProps.length ||
|
||||
normalized.some((property, index) => property !== rawCardProps[index])
|
||||
@@ -129,7 +135,8 @@ export function normalizeLoadedUiState(
|
||||
migratedCardProps !== undefined ||
|
||||
!inlineAgentsMigrated ||
|
||||
!expandedCardPropsMigrated ||
|
||||
!jiraIssueCardPropDefaulted
|
||||
!jiraIssueCardPropDefaulted ||
|
||||
!hostCardPropDefaulted
|
||||
) {
|
||||
markNeedsSave()
|
||||
}
|
||||
@@ -213,6 +220,7 @@ export function normalizeLoadedUiState(
|
||||
_inlineAgentsDefaultedForExperiment: true,
|
||||
_inlineAgentsDefaultedForAllUsers: true,
|
||||
_expandedWorktreeCardPropertiesDefaulted: true,
|
||||
_jiraIssueWorktreeCardPropertyDefaulted: true
|
||||
_jiraIssueWorktreeCardPropertyDefaulted: true,
|
||||
_hostWorktreeCardPropertyDefaulted: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ type MainOwnedUIState =
|
||||
| 'dashboardPopoutBounds'
|
||||
| '_expandedWorktreeCardPropertiesDefaulted'
|
||||
| '_jiraIssueWorktreeCardPropertyDefaulted'
|
||||
| '_hostWorktreeCardPropertyDefaulted'
|
||||
| 'starNagBaselineAgents'
|
||||
| 'starNagAppVersion'
|
||||
| 'starNagNextThreshold'
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import type { ReactNode } from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { GlobalSettings } from '../../../../shared/global-settings-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
|
||||
const fetchHostedReviewForBranch = vi.fn()
|
||||
const fetchIssue = vi.fn()
|
||||
const fetchLinearIssue = vi.fn()
|
||||
const openModal = vi.fn()
|
||||
const updateWorktreeMeta = vi.fn()
|
||||
|
||||
const HOST_LABEL = 'gpu-vm'
|
||||
let worktreeCardProperties: WorktreeCardProperty[] = []
|
||||
let settings: Partial<GlobalSettings> | null = null
|
||||
const WORKTREE_CARD_IMPORT_TIMEOUT_MS = 15_000
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
deleteStateByWorktreeId: {},
|
||||
fetchHostedReviewForBranch,
|
||||
fetchIssue,
|
||||
fetchLinearIssue,
|
||||
gitConflictOperationByWorktree: {},
|
||||
hostedReviewCache: {},
|
||||
issueCache: {},
|
||||
linearIssueCache: {},
|
||||
openModal,
|
||||
projectGroups: [],
|
||||
remoteBranchConflictByWorktreeId: {},
|
||||
settings,
|
||||
sshConnectionStates: new Map(),
|
||||
sshTargetLabels: new Map(),
|
||||
updateWorktreeMeta,
|
||||
workspacePortScan: null,
|
||||
worktreeCardProperties
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-activation', () => ({
|
||||
activateAndRevealWorktree: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipContent: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
|
||||
}))
|
||||
|
||||
vi.mock('./use-worktree-activity-status', () => ({
|
||||
useWorktreeActivityStatus: () => 'idle'
|
||||
}))
|
||||
|
||||
vi.mock('./use-worktree-sleep-state', () => ({
|
||||
useIsSleepingWorktree: () => false
|
||||
}))
|
||||
|
||||
vi.mock('./CacheTimer', () => ({
|
||||
default: () => null,
|
||||
usePromptCacheCountdownStartedAt: () => null
|
||||
}))
|
||||
|
||||
vi.mock('./WorktreeCardAgents', () => ({
|
||||
default: () => null
|
||||
}))
|
||||
|
||||
vi.mock('./WorktreeContextMenu', () => ({
|
||||
default: ({ children }: { children: ReactNode }) => <>{children}</>,
|
||||
CLOSE_ALL_CONTEXT_MENUS_EVENT: 'orca:test-close-context-menus',
|
||||
WORKTREE_NATIVE_CONTEXT_MENU_ATTR: 'data-worktree-native-context-menu',
|
||||
WORKTREE_CONTEXT_MENU_SCOPE_ATTR: 'data-orca-context-menu-scope'
|
||||
}))
|
||||
|
||||
function makeRepo(overrides: Partial<Repo> = {}): Repo {
|
||||
return {
|
||||
id: 'repo-1',
|
||||
path: '/repo',
|
||||
displayName: 'orca',
|
||||
badgeColor: '#999999',
|
||||
repoIcon: { type: 'emoji', emoji: '🦊' },
|
||||
addedAt: 1,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function makeWorktree(overrides: Partial<Worktree> = {}): Worktree {
|
||||
return {
|
||||
id: 'repo-1::/repo/worktrees/host',
|
||||
repoId: 'repo-1',
|
||||
path: '/repo/worktrees/host',
|
||||
displayName: 'Host tree',
|
||||
branch: 'feature/host',
|
||||
head: 'abc123',
|
||||
isBare: false,
|
||||
isMainWorktree: false,
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 1,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
// Imported per test so the mocked store is seeded before the module reads it.
|
||||
async function renderCardMarkup(): Promise<string> {
|
||||
const { default: WorktreeCard } = await import('./WorktreeCard')
|
||||
return renderToStaticMarkup(
|
||||
<WorktreeCard
|
||||
worktree={makeWorktree()}
|
||||
repo={makeRepo()}
|
||||
isActive={false}
|
||||
hostContextLabel={HOST_LABEL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('WorktreeCard host badge toggle', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
worktreeCardProperties = []
|
||||
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false }
|
||||
})
|
||||
|
||||
it(
|
||||
'shows the execution-host label when the host card property is enabled',
|
||||
async () => {
|
||||
worktreeCardProperties = ['host']
|
||||
|
||||
expect(await renderCardMarkup()).toContain(HOST_LABEL)
|
||||
},
|
||||
WORKTREE_CARD_IMPORT_TIMEOUT_MS
|
||||
)
|
||||
|
||||
it(
|
||||
'hides the execution-host label when the host card property is off',
|
||||
async () => {
|
||||
worktreeCardProperties = ['status']
|
||||
|
||||
expect(await renderCardMarkup()).not.toContain(HOST_LABEL)
|
||||
},
|
||||
WORKTREE_CARD_IMPORT_TIMEOUT_MS
|
||||
)
|
||||
})
|
||||
@@ -123,6 +123,13 @@ const BASE_WORKTREE_CARD_PROPERTY_OPTIONS: WorktreeCardPropertyOption[] = [
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'host',
|
||||
properties: ['host'],
|
||||
get label() {
|
||||
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.host', 'Host')
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'branch',
|
||||
properties: ['branch'],
|
||||
|
||||
@@ -67,5 +67,11 @@ export const PROPERTY_OPTIONS: { id: WorktreeCardProperty; label: string }[] = [
|
||||
'Agent activity'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'host',
|
||||
get label() {
|
||||
return translate('auto.components.sidebar.SidebarWorkspaceOptionsMenu.host', 'Host')
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -77,7 +77,7 @@ export function buildWorktreeCardPresentation(card: WorktreeCardController) {
|
||||
showRepoIdentityInTitle && !!repo && !hideRepoBadge && !isFolder && !showPinnedRepoIcon
|
||||
const showRepoBadgeInMetaRow =
|
||||
!showRepoIdentityInTitle && !!repo && !hideRepoBadge && !showPinnedRepoIcon
|
||||
const showHostContextBadge = !compactCards && !!hostContextLabel
|
||||
const showHostContextBadge = !compactCards && !!hostContextLabel && cardProps.includes('host')
|
||||
const showDetachedHeadInMetaRow = !compactCards && !isFolder && detachedHeadDisplay !== null
|
||||
const showBranch =
|
||||
!isFolder &&
|
||||
|
||||
@@ -5582,6 +5582,7 @@
|
||||
"bdd23b4e07": "GitHub issues",
|
||||
"44713a5d04": "Linear issues",
|
||||
"jiraIssues": "Jira issues",
|
||||
"host": "Host",
|
||||
"cc17bd443b": "Detailed",
|
||||
"0f9b959b31": "PR",
|
||||
"e029a2d775": "Status",
|
||||
|
||||
@@ -4603,6 +4603,7 @@
|
||||
"bdd23b4e07": "Issues de GitHub",
|
||||
"44713a5d04": "Issues de Linear",
|
||||
"jiraIssues": "Issues de Jira",
|
||||
"host": "Host",
|
||||
"cc17bd443b": "Detallado",
|
||||
"0f9b959b31": "PR",
|
||||
"e029a2d775": "Estado",
|
||||
|
||||
@@ -5515,6 +5515,7 @@
|
||||
"bdd23b4e07": "Issues GitHub",
|
||||
"44713a5d04": "Issues Linear",
|
||||
"jiraIssues": "Tickets Jira",
|
||||
"host": "Hôte",
|
||||
"cc17bd443b": "Détaillé",
|
||||
"0f9b959b31": "PR",
|
||||
"e029a2d775": "Statut",
|
||||
|
||||
@@ -5361,6 +5361,7 @@
|
||||
"bdd23b4e07": "GitHub Issue",
|
||||
"44713a5d04": "Linear Issue",
|
||||
"jiraIssues": "Jira Issue",
|
||||
"host": "ホスト",
|
||||
"cc17bd443b": "詳細",
|
||||
"0f9b959b31": "PR",
|
||||
"e029a2d775": "状態",
|
||||
|
||||
@@ -5361,6 +5361,7 @@
|
||||
"bdd23b4e07": "GitHub 이슈",
|
||||
"44713a5d04": "Linear 이슈",
|
||||
"jiraIssues": "Jira 이슈",
|
||||
"host": "호스트",
|
||||
"cc17bd443b": "상세한",
|
||||
"0f9b959b31": "PR",
|
||||
"e029a2d775": "상태",
|
||||
|
||||
@@ -5361,6 +5361,7 @@
|
||||
"bdd23b4e07": "GitHub 议题",
|
||||
"44713a5d04": "Linear 议题",
|
||||
"jiraIssues": "Jira 议题",
|
||||
"host": "主机",
|
||||
"cc17bd443b": "详细的",
|
||||
"0f9b959b31": "PR",
|
||||
"e029a2d775": "状态",
|
||||
|
||||
@@ -196,6 +196,8 @@ export type PersistedUIState = {
|
||||
_expandedWorktreeCardPropertiesDefaulted?: boolean
|
||||
/** One-shot backfill flag for 'jira-issue', which joined the defaults after the expansion migration had already stamped upgraded profiles. */
|
||||
_jiraIssueWorktreeCardPropertyDefaulted?: boolean
|
||||
/** One-shot backfill flag for 'host', which became a toggleable property after earlier profiles were already stamped. */
|
||||
_hostWorktreeCardPropertyDefaulted?: boolean
|
||||
/** totalAgentsSpawned snapshot at first sighting of the current app version, so the nag counts agents since last update (not from zero). */
|
||||
starNagBaselineAgents?: number | null
|
||||
/** App version that set the current baseline; a version change re-captures the baseline on next spawn, restarting the nag countdown. */
|
||||
|
||||
@@ -44,6 +44,8 @@ export type WorktreeCardProperty =
|
||||
| 'ports'
|
||||
// Inline agent-activity list rendered in each workspace card; on by default (see DEFAULT_WORKTREE_CARD_PROPERTIES in shared/constants.ts).
|
||||
| 'inline-agents'
|
||||
// Execution-host pill, shown only when the visible workspaces span more than one host; on by default.
|
||||
| 'host'
|
||||
|
||||
export type WorktreeCardMode = 'Default' | 'Compact'
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ export const DEFAULT_WORKTREE_CARD_PROPERTIES: WorktreeCardProperty[] = [
|
||||
// Why: agent activity is the primary reason users opt into the feature, so
|
||||
// the Default mode keeps it inline on each card while Compact removes the
|
||||
// extra row.
|
||||
'inline-agents'
|
||||
'inline-agents',
|
||||
'host'
|
||||
]
|
||||
|
||||
// Why: compact cards default to the quiet preset; metadata icons remain opt-in
|
||||
@@ -55,7 +56,8 @@ export const WORKTREE_CARD_PROPERTIES = [
|
||||
'cli',
|
||||
'comment',
|
||||
'ports',
|
||||
'inline-agents'
|
||||
'inline-agents',
|
||||
'host'
|
||||
] as const satisfies readonly WorktreeCardProperty[]
|
||||
|
||||
export function normalizeWorktreeCardProperties(
|
||||
|
||||
Reference in New Issue
Block a user