fix(macos): keep the fix dialog's steps a checklist and put the one action in the footer

Buttons inside each step made the list look like a form, and a footer Close duplicated
the X. The footer now carries the active step's action, with a ghost Cancel; a probe
that could not answer says so under step 1 instead of showing a check.
This commit is contained in:
Jinwoo-H
2026-09-21 04:22:43 -04:00
parent 5841a07ca3
commit adfe2ee53d
3 changed files with 96 additions and 57 deletions
@@ -30,13 +30,12 @@ function restartButton(): HTMLElement {
return screen.getByRole('button', { name: /^Restart/ })
}
/** Scoped to the footer: DialogContent's own dismiss X carries the same accessible name. */
function footerCloseButton(): HTMLElement {
function footerButton(name: string): HTMLElement {
const footer = screen.getByRole('dialog').querySelector('[data-slot="dialog-footer"]')
if (!(footer instanceof HTMLElement)) {
throw new Error('dialog footer did not render')
}
return within(footer).getByRole('button', { name: 'Close' })
return within(footer).getByRole('button', { name })
}
beforeEach(() => {
@@ -83,27 +82,28 @@ describe('MacFolderAccessFixDialog', () => {
expect(restartButton().hasAttribute('disabled')).toBe(false)
})
it('opens step one and blocks Restart when Orca itself is denied', () => {
it('makes System Settings the only action when Orca itself is denied', () => {
openWith(false)
render(<MacFolderAccessFixDialog />)
expect(screen.getByRole('button', { name: 'Open System Settings' })).toBeTruthy()
expect(restartButton().hasAttribute('disabled')).toBe(true)
expect(footerButton('Open System Settings')).toBeTruthy()
expect(screen.queryByRole('button', { name: /^Restart/ })).toBeNull()
})
// An unanswered probe must not accuse the user of a missing grant, but the pane stays reachable.
it('keeps both steps available when the probe could not answer', () => {
it('keeps both actions and says so when the probe could not answer', () => {
openWith(null)
render(<MacFolderAccessFixDialog />)
expect(screen.getByRole('button', { name: 'Open System Settings' })).toBeTruthy()
expect(footerButton('Open System Settings')).toBeTruthy()
expect(restartButton().hasAttribute('disabled')).toBe(false)
expect(screen.getByText(/Orca couldnt check this/)).toBeTruthy()
})
it('flips step one to done when a later poll reports the grant landed', async () => {
openWith(false)
render(<MacFolderAccessFixDialog />)
expect(restartButton().hasAttribute('disabled')).toBe(true)
expect(screen.queryByRole('button', { name: /^Restart/ })).toBeNull()
act(() => {
useMacFolderAccessFixStore.getState().observeMismatch({
@@ -171,7 +171,9 @@ describe('MacFolderAccessFixDialog', () => {
await waitFor(() => {
expect(
screen.getByText('Done. Terminals opened in your Documents folder can read it now.')
screen.getByText(
'Terminal service restarted. Terminals in your Documents folder should work now.'
)
).toBeTruthy()
})
expect(screen.queryByRole('button', { name: /^Restart/ })).toBeNull()
@@ -209,11 +211,11 @@ describe('MacFolderAccessFixDialog', () => {
expect(restartButton().hasAttribute('disabled')).toBe(false)
})
it('closes on Close', async () => {
it('closes on Cancel', async () => {
openWith(true)
render(<MacFolderAccessFixDialog />)
await userEvent.click(footerCloseButton())
await userEvent.click(footerButton('Cancel'))
expect(useMacFolderAccessFixStore.getState().open).toBe(false)
})
@@ -23,13 +23,11 @@ type RestartState = 'idle' | 'busy' | 'done' | 'failed'
function Step({
done,
label,
helper,
action
helper
}: {
done: boolean
label: string
helper?: string
action?: React.ReactNode
}): React.JSX.Element {
return (
<li className="flex items-start gap-2">
@@ -38,10 +36,9 @@ function Step({
) : (
<CircleDashed className="mt-0.5 size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
)}
<div className="flex min-w-0 flex-1 flex-col items-start gap-1.5">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-sm text-foreground">{label}</span>
{helper ? <span className="text-xs text-muted-foreground">{helper}</span> : null}
{action}
</div>
</li>
)
@@ -49,38 +46,29 @@ function Step({
function FixSteps({
mismatch,
restartState,
onOpenSettings,
onRestart
restartState
}: {
mismatch: PtyManagementFolderAccessMismatch
restartState: RestartState
onOpenSettings: () => void
onRestart: () => void
}): React.JSX.Element {
// Why `!== false`: a probe that could not answer must not accuse the user of a missing grant.
const allowed = mismatch.restartWillHelp !== false
const busy = restartState === 'busy'
return (
<>
<ol className="flex flex-col gap-3">
<Step
done={allowed}
done={mismatch.restartWillHelp === true}
label={translate(
'auto.components.shared.MacFolderAccessFixDialog.stepAllow',
'Allow Orca under Files and Folders'
)}
action={
// Why keep the button on an unanswered probe: opening the pane is the one step the
// user can always take, and hiding it would strand them.
mismatch.restartWillHelp === true ? undefined : (
<Button variant="outline" size="sm" onClick={onOpenSettings}>
{translate(
'auto.components.shared.MacFolderAccessFixDialog.openSystemSettings',
'Open System Settings'
)}
</Button>
)
helper={
// Why only when unanswered: a probe that could not answer must not accuse the user of a
// missing grant, but it must say why the step is left to them.
mismatch.restartWillHelp === null
? translate(
'auto.components.shared.MacFolderAccessFixDialog.stepAllowUnknown',
'Orca couldnt check this. Skip it if Orca is already allowed.'
)
: undefined
}
/>
<Step
@@ -93,17 +81,6 @@ function FixSteps({
'auto.components.shared.MacFolderAccessFixDialog.restartConsequence',
'Open terminals and agents will restart.'
)}
action={
<Button size="sm" onClick={onRestart} disabled={!allowed || busy}>
{busy ? <LoaderCircle className="size-4 animate-spin" /> : null}
{busy
? translate(
'auto.components.shared.MacFolderAccessFixDialog.restarting',
'Restarting…'
)
: translate('auto.components.shared.MacFolderAccessFixDialog.restart', 'Restart')}
</Button>
}
/>
</ol>
{restartState === 'failed' ? (
@@ -118,6 +95,65 @@ function FixSteps({
)
}
/** The footer carries the active step's one action, so the steps stay a checklist. */
function FixFooter({
mismatch,
restartState,
onCancel,
onOpenSettings,
onRestart
}: {
mismatch: PtyManagementFolderAccessMismatch
restartState: RestartState
onCancel: () => void
onOpenSettings: () => void
onRestart: () => void
}): React.JSX.Element {
const busy = restartState === 'busy'
const openSettingsLabel = translate(
'auto.components.shared.MacFolderAccessFixDialog.openSystemSettings',
'Open System Settings'
)
if (restartState === 'done') {
return (
<Button size="sm" onClick={onCancel}>
{translate('auto.components.shared.MacFolderAccessFixDialog.close', 'Close')}
</Button>
)
}
if (mismatch.restartWillHelp === false) {
return (
<>
<Button variant="ghost" size="sm" onClick={onCancel}>
{translate('auto.components.shared.MacFolderAccessFixDialog.cancel', 'Cancel')}
</Button>
<Button size="sm" onClick={onOpenSettings}>
{openSettingsLabel}
</Button>
</>
)
}
return (
<>
{mismatch.restartWillHelp === null ? (
<Button variant="ghost" size="sm" onClick={onOpenSettings} disabled={busy}>
{openSettingsLabel}
</Button>
) : (
<Button variant="ghost" size="sm" onClick={onCancel} disabled={busy}>
{translate('auto.components.shared.MacFolderAccessFixDialog.cancel', 'Cancel')}
</Button>
)}
<Button size="sm" onClick={onRestart} disabled={busy}>
{busy ? <LoaderCircle className="size-4 animate-spin" /> : null}
{busy
? translate('auto.components.shared.MacFolderAccessFixDialog.restarting', 'Restarting…')
: translate('auto.components.shared.MacFolderAccessFixDialog.restart', 'Restart')}
</Button>
</>
)
}
/**
* The remedy for a daemon macOS refuses a folder to (STA-7948), raised from the folder-access
* toast. Two steps, because a restart alone only works once Orca itself is allowed again — which
@@ -209,22 +245,21 @@ export function MacFolderAccessFixDialog(): React.JSX.Element | null {
<p className="text-sm text-foreground">
{translate(
'auto.components.shared.MacFolderAccessFixDialog.done',
'Done. Terminals opened in your {{folder}} can read it now.',
'Terminal service restarted. Terminals in your {{folder}} should work now.',
{ folder }
)}
</p>
) : (
<FixSteps
<FixSteps mismatch={mismatch} restartState={restartState} />
)}
<DialogFooter>
<FixFooter
mismatch={mismatch}
restartState={restartState}
onCancel={close}
onOpenSettings={onOpenSettings}
onRestart={() => void onRestart()}
/>
)}
<DialogFooter>
<Button variant="outline" onClick={close} disabled={busy}>
{translate('auto.components.shared.MacFolderAccessFixDialog.close', 'Close')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
+4 -2
View File
@@ -6414,8 +6414,10 @@
"restartFailed": "Restart failed. Try again from Settings → Terminal → Manage Sessions.",
"title": "Fix access to your {{folder}}",
"lead": "macOS is blocking Orcas terminal service from this folder.",
"done": "Done. Terminals opened in your {{folder}} can read it now.",
"close": "Close"
"done": "Terminal service restarted. Terminals in your {{folder}} should work now.",
"close": "Close",
"cancel": "Cancel",
"stepAllowUnknown": "Orca couldnt check this. Skip it if Orca is already allowed."
},
"macFolderAccessFolderName": {
"documents": "Documents folder",