Clarify macOS access prompt guidance (#11807)
* Clarify macOS access prompt guidance * Make settings target highlight visible
This commit is contained in:
parent
90963f8ee3
commit
7d24dad48a
|
|
@ -0,0 +1,52 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import type { DeveloperPermissionState } from '../../../../shared/developer-permissions-types'
|
||||
import { FULL_DISK_ACCESS_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
|
||||
import { DeveloperPermissionsPane } from './DeveloperPermissionsPane'
|
||||
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
beforeEach(() => {
|
||||
Object.assign(window, {
|
||||
api: {
|
||||
developerPermissions: {
|
||||
getStatus: vi.fn(
|
||||
async (): Promise<DeveloperPermissionState[]> => [
|
||||
{ id: 'full-disk-access', status: 'denied' }
|
||||
]
|
||||
),
|
||||
request: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount())
|
||||
container.remove()
|
||||
Reflect.deleteProperty(window, 'api')
|
||||
})
|
||||
|
||||
it('highlights the Full Disk Access row for a targeted Settings navigation', async () => {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<DeveloperPermissionsPane highlightedSettingId={FULL_DISK_ACCESS_SETTINGS_TARGET_ID} />
|
||||
)
|
||||
})
|
||||
|
||||
const row = container.querySelector<HTMLElement>(
|
||||
`[data-settings-section="${FULL_DISK_ACCESS_SETTINGS_TARGET_ID}"]`
|
||||
)
|
||||
expect(row?.dataset.highlighted).toBe('true')
|
||||
expect(row?.className).toContain('data-[highlighted=true]:ring-annotation-highlight/60')
|
||||
|
||||
await act(async () => root.render(<DeveloperPermissionsPane />))
|
||||
expect(row?.dataset.highlighted).toBeUndefined()
|
||||
})
|
||||
|
|
@ -23,6 +23,10 @@ import { Button } from '../ui/button'
|
|||
import { translate } from '@/i18n/i18n'
|
||||
export { getDeveloperPermissionsPaneSearchEntries } from './developer-permissions-search'
|
||||
|
||||
type DeveloperPermissionsPaneProps = {
|
||||
highlightedSettingId?: string | null
|
||||
}
|
||||
|
||||
type PermissionDefinition = {
|
||||
id: DeveloperPermissionId
|
||||
label: string
|
||||
|
|
@ -202,7 +206,9 @@ function statusClass(status: DeveloperPermissionStatus | undefined): string {
|
|||
return 'border-border bg-muted text-muted-foreground'
|
||||
}
|
||||
|
||||
export function DeveloperPermissionsPane(): React.JSX.Element {
|
||||
export function DeveloperPermissionsPane({
|
||||
highlightedSettingId = null
|
||||
}: DeveloperPermissionsPaneProps): React.JSX.Element {
|
||||
const [states, setStates] = useState<DeveloperPermissionState[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [pendingId, setPendingId] = useState<DeveloperPermissionId | null>(null)
|
||||
|
|
@ -340,9 +346,15 @@ export function DeveloperPermissionsPane(): React.JSX.Element {
|
|||
{PERMISSIONS.map((permission) => {
|
||||
const status = stateById.get(permission.id)
|
||||
const pending = pendingId === permission.id
|
||||
const settingId = `developer-permissions-${permission.id}`
|
||||
|
||||
return (
|
||||
<div key={permission.id} className="flex items-center justify-between gap-4 px-4 py-3">
|
||||
<div
|
||||
key={permission.id}
|
||||
data-settings-section={settingId}
|
||||
data-highlighted={highlightedSettingId === settingId ? 'true' : undefined}
|
||||
className="flex items-center justify-between gap-4 px-4 py-3 transition-[background-color,box-shadow] duration-500 data-[highlighted=true]:bg-annotation-highlight/10 data-[highlighted=true]:ring-2 data-[highlighted=true]:ring-inset data-[highlighted=true]:ring-annotation-highlight/60 motion-reduce:transition-none"
|
||||
>
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<div className="mt-0.5 text-muted-foreground">{permission.icon}</div>
|
||||
<div className="min-w-0 space-y-1">
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ const SETTINGS_NAV_GROUP_BY_ID = new Map<string, SettingsNavGroupDefinition>(
|
|||
|
||||
const SHORTCUTS_ESCAPE_CONFIRM_TOAST_ID = 'shortcuts-escape-confirm'
|
||||
const SHORTCUTS_ESCAPE_CONFIRM_WINDOW_MS = 2200
|
||||
const SETTINGS_TARGET_HIGHLIGHT_MS = 3_000
|
||||
|
||||
function getSettingsSectionId(
|
||||
pane: SettingsNavTarget,
|
||||
|
|
@ -372,6 +373,9 @@ function Settings(): React.JSX.Element {
|
|||
getInitialMountedSectionIds
|
||||
)
|
||||
const [pendingNavRequestTick, setPendingNavRequestTick] = useState(0)
|
||||
const [highlightedSettingsTargetId, setHighlightedSettingsTargetId] = useState<string | null>(
|
||||
null
|
||||
)
|
||||
const [quickCommandAddIntentSignal, setQuickCommandAddIntentSignal] = useState(0)
|
||||
const [sshHostAddIntentSignal, setSshHostAddIntentSignal] = useState(0)
|
||||
const [remoteServerAddIntentSignal, setRemoteServerAddIntentSignal] = useState(0)
|
||||
|
|
@ -446,6 +450,17 @@ function Settings(): React.JSX.Element {
|
|||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!highlightedSettingsTargetId) {
|
||||
return
|
||||
}
|
||||
const timeout = window.setTimeout(
|
||||
() => setHighlightedSettingsTargetId(null),
|
||||
SETTINGS_TARGET_HIGHLIGHT_MS
|
||||
)
|
||||
return () => window.clearTimeout(timeout)
|
||||
}, [highlightedSettingsTargetId])
|
||||
|
||||
const requestFontSuggestions = useCallback((): void => {
|
||||
if (installedFontsLoadedRef.current || installedFontsLoadPromiseRef.current) {
|
||||
return
|
||||
|
|
@ -661,6 +676,11 @@ function Settings(): React.JSX.Element {
|
|||
}
|
||||
pendingNavSectionRef.current = paneSectionId
|
||||
pendingScrollTargetRef.current = settingsNavigationTarget.sectionId ?? paneSectionId
|
||||
setHighlightedSettingsTargetId(
|
||||
settingsNavigationTarget.pane === 'developer-permissions'
|
||||
? (settingsNavigationTarget.sectionId ?? null)
|
||||
: null
|
||||
)
|
||||
// Why: ensure Appearance's nested status-bar section is open before scrolling so the row is visible.
|
||||
if (settingsNavigationTarget.pane === 'appearance') {
|
||||
const accordion = resolveAppearanceAccordionDeepLink(settingsNavigationTarget.sectionId)
|
||||
|
|
@ -1696,7 +1716,9 @@ function Settings(): React.JSX.Element {
|
|||
searchEntries={getSectionSearchEntries('developer-permissions')}
|
||||
>
|
||||
{isSectionMounted('developer-permissions') ? (
|
||||
<DeveloperPermissionsPane />
|
||||
<DeveloperPermissionsPane
|
||||
highlightedSettingId={highlightedSettingsTargetId}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { UI_LANGUAGE_SPANISH } from '../../../shared/ui-language'
|
|||
import { useAppStore } from '@/store'
|
||||
import { usePluginLanguagePackStore } from '@/store/plugin-language-packs'
|
||||
import { i18n } from '@/i18n/i18n'
|
||||
import { FULL_DISK_ACCESS_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
|
||||
import { MacosTccPromptNoticeHost } from './MacosTccPromptNoticeHost'
|
||||
import { useMacosTccPromptNotice } from './useMacosTccPromptNotice'
|
||||
|
||||
|
|
@ -95,7 +96,7 @@ it('isolates plugin language-pack discovery from its parent render path', async
|
|||
expect(subscribeToMacosTccPromptNotice).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps the notice open until the user closes it', async () => {
|
||||
it('connects the notice to the macOS prompt and keeps it open until closed', async () => {
|
||||
useAppStore.setState({
|
||||
settings: { ...getDefaultSettings('/tmp'), uiLanguage: 'en' }
|
||||
})
|
||||
|
|
@ -111,6 +112,13 @@ it('keeps the notice open until the user closes it', async () => {
|
|||
|
||||
showNotice?.({ promptCount: 1 }, acknowledge)
|
||||
|
||||
expect(toastWarning).toHaveBeenCalledWith(
|
||||
'Seeing “Orca would like to access…”?',
|
||||
expect.objectContaining({
|
||||
description:
|
||||
'That macOS message appears when an agent or terminal tool accesses protected files. macOS names Orca because Orca launched the tool. Grant Full Disk Access to Orca and Orca Helper to reduce future prompts.'
|
||||
})
|
||||
)
|
||||
const options = toastWarning.mock.calls[0]?.[1] as
|
||||
| { duration?: number; onDismiss?: () => void }
|
||||
| undefined
|
||||
|
|
@ -141,4 +149,9 @@ it('acknowledges when opening Settings closes the notice', async () => {
|
|||
| undefined
|
||||
options?.action?.onClick()
|
||||
expect(acknowledge).toHaveBeenCalledOnce()
|
||||
expect(useAppStore.getState().settingsNavigationTarget).toEqual({
|
||||
pane: 'developer-permissions',
|
||||
repoId: null,
|
||||
sectionId: FULL_DISK_ACCESS_SETTINGS_TARGET_ID
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useAppStore } from '@/store'
|
|||
import { usePluginLanguagePackStore } from '@/store/plugin-language-packs'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { resolveUiLocale } from '@/i18n/supported-languages'
|
||||
import { FULL_DISK_ACCESS_SETTINGS_TARGET_ID } from '@/lib/settings-navigation-types'
|
||||
import {
|
||||
dismissMacosTccPromptNotice,
|
||||
subscribeToMacosTccPromptNotice
|
||||
|
|
@ -41,12 +42,12 @@ export function useMacosTccPromptNotice(): void {
|
|||
toast.warning(
|
||||
translate(
|
||||
'auto.hooks.useMacosTccPromptNotice.title',
|
||||
'Reduce repeated macOS file-access prompts'
|
||||
'Seeing “Orca would like to access…”?'
|
||||
),
|
||||
{
|
||||
description: translate(
|
||||
'auto.hooks.useMacosTccPromptNotice.description',
|
||||
'macOS attributes file access by your agents and terminal tools to Orca. Granting Full Disk Access reduces these prompts.'
|
||||
'That macOS message appears when an agent or terminal tool accesses protected files. macOS names Orca because Orca launched the tool. Grant Full Disk Access to Orca and Orca Helper to reduce future prompts.'
|
||||
),
|
||||
duration: Infinity,
|
||||
onDismiss: acknowledge,
|
||||
|
|
@ -55,7 +56,11 @@ export function useMacosTccPromptNotice(): void {
|
|||
onClick: () => {
|
||||
acknowledge()
|
||||
openSettingsPage()
|
||||
openSettingsTarget({ pane: 'developer-permissions', repoId: null })
|
||||
openSettingsTarget({
|
||||
pane: 'developer-permissions',
|
||||
repoId: null,
|
||||
sectionId: FULL_DISK_ACCESS_SETTINGS_TARGET_ID
|
||||
})
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
|
|
|
|||
|
|
@ -826,8 +826,8 @@
|
|||
"pasteTooLarge": "Paste is too large."
|
||||
},
|
||||
"useMacosTccPromptNotice": {
|
||||
"title": "Reduce repeated macOS file-access prompts",
|
||||
"description": "macOS attributes file access by your agents and terminal tools to Orca. Granting Full Disk Access reduces these prompts.",
|
||||
"title": "Seeing “Orca would like to access…”?",
|
||||
"description": "That macOS message appears when an agent or terminal tool accesses protected files. macOS names Orca because Orca launched the tool. Grant Full Disk Access to Orca and Orca Helper to reduce future prompts.",
|
||||
"openSettings": "Open Settings",
|
||||
"dismiss": "Don't show again"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ const SETTINGS_NAV_TARGET_SET: ReadonlySet<string> = new Set(SETTINGS_NAV_TARGET
|
|||
const SETTINGS_NAV_INTENT_SET: ReadonlySet<string> = new Set(SETTINGS_NAV_INTENTS)
|
||||
|
||||
export type SettingsNavTarget = (typeof SETTINGS_NAV_TARGETS)[number]
|
||||
export const FULL_DISK_ACCESS_SETTINGS_TARGET_ID = 'developer-permissions-full-disk-access'
|
||||
|
||||
export type SettingsNavigationTarget = {
|
||||
pane: SettingsNavTarget
|
||||
repoId: string | null
|
||||
|
|
|
|||
Loading…
Reference in New Issue