From c86a87069b3c2768677b9049b29012b9fb1fd8b4 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 22 Jun 2026 22:35:11 +0900 Subject: [PATCH 1/7] Stack permission dialog buttons in a centered vertical column NSAlert lays its buttons out in a horizontal row, which overlaps once the dialog is widened to fit a long command and the larger dialog font. Add a stackAlertButtonsVertically pass that reparents the buttons onto the content view, grows the window to make room, and lays them out as a single vertical column centered horizontally and vertically. Each button keeps its own fitted width so the layout survives the relayout NSAlert runs while presenting, and the existing reverse add-order keeps the top-to-bottom order matching the button list and the return-code mapping intact. --- src/agent_hooks/assets/dialog.applescript | 99 ++++++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/src/agent_hooks/assets/dialog.applescript b/src/agent_hooks/assets/dialog.applescript index c6618f0..08495be 100644 --- a/src/agent_hooks/assets/dialog.applescript +++ b/src/agent_hooks/assets/dialog.applescript @@ -16,8 +16,9 @@ on run argv alert's setMessageText:theTitle alert's setInformativeText:theMessage -- NSAlert anchors the first-added button on the right. Add buttons in reverse - -- so the on-screen order (left to right) matches buttonList order, and let - -- NSAlert lay them out natively to avoid overlap and hit-test mismatches. + -- so that once stackAlertButtonsVertically lays them out as a vertical column + -- (placing the first-added button at the bottom), the visible top-to-bottom + -- order matches buttonList order. repeat with i from (count of buttonList) to 1 by -1 alert's addButtonWithTitle:((item i of buttonList) as text) end repeat @@ -27,6 +28,7 @@ on run argv my clearButtonDefaultState(alert) alert's layout() my setAlertWidthForVisibleLines(alert, theMessage, theFontSize) + my stackAlertButtonsVertically(alert) set responseCode to (alert's runModal()) as integer -- Buttons were added in reverse, so map the return code back to buttonList. @@ -38,6 +40,99 @@ on run argv return "button returned:" & (item buttonIndex of buttonList) end run +on stackAlertButtonsVertically(alert) + -- Lay the buttons out as a single vertical column centered in the dialog. NSAlert + -- packs buttons into a horizontal row by default, which overlaps once the dialog + -- is widened to fit a long message and the larger dialog font. Stacking top to + -- bottom removes that failure mode regardless of dialog width or button count. + set buttonViews to alert's buttons() + set buttonCount to (count of buttonViews) + if buttonCount is less than 2 then return + + set theWindow to alert's |window|() + set contentView to theWindow's contentView() + set contentWidth to (item 1 of item 2 of (contentView's frame())) as real + + set gap to 8 + set sideMargin to 20 + + -- Measure where the current button band sits and how tall the tallest button is, + -- in content-view coordinates, before anything is moved. + set bandBottom to missing value + set bandTop to missing value + set buttonHeight to 0 + repeat with theButton in buttonViews + theButton's sizeToFit() + set bandRect to (theButton's superview())'s convertRect:(theButton's frame()) toView:contentView + set rectBottom to (item 2 of item 1 of bandRect) as real + set rectHeight to (item 2 of item 2 of bandRect) as real + set rectTop to rectBottom + rectHeight + if bandBottom is missing value or rectBottom < bandBottom then set bandBottom to rectBottom + if bandTop is missing value or rectTop > bandTop then set bandTop to rectTop + if rectHeight > buttonHeight then set buttonHeight to rectHeight + end repeat + + set columnHeight to (buttonCount * buttonHeight) + ((buttonCount - 1) * gap) + set extraHeight to columnHeight - (bandTop - bandBottom) + if extraHeight is less than 0 then set extraHeight to 0 + + -- Reparent the buttons directly onto the content view so they can be positioned + -- in absolute coordinates, then drop the now-empty native button container. + set buttonContainer to (item 1 of buttonViews)'s superview() + if buttonContainer is not contentView then + repeat with theButton in buttonViews + theButton's removeFromSuperview() + contentView's addSubview:theButton + end repeat + buttonContainer's removeFromSuperview() + end if + + -- Pin the existing content to the top so growing the window adds room at the + -- bottom for the taller column rather than stretching the message area. + if extraHeight is greater than 0 then + repeat with childView in (contentView's subviews()) + childView's setAutoresizingMask:8 -- NSViewMinYMargin: stay anchored to the top + end repeat + set windowFrame to theWindow's frame() + set grownFrame to current application's NSMakeRect((item 1 of item 1 of windowFrame), ((item 2 of item 1 of windowFrame) - extraHeight), (item 1 of item 2 of windowFrame), ((item 2 of item 2 of windowFrame) + extraHeight)) + theWindow's setFrame:grownFrame display:false + end if + + -- Find the lowest piece of content (the divider NSAlert draws above the buttons, + -- or the message text) so the column can be centered in the space between it and + -- the bottom of the dialog. + set contentFloor to missing value + repeat with childView in (contentView's subviews()) + if not ((childView's isKindOfClass:(current application's NSButton)) as boolean) then + set childBottom to (item 2 of item 1 of (childView's frame())) as real + if contentFloor is missing value or childBottom < contentFloor then set contentFloor to childBottom + end if + end repeat + if contentFloor is missing value then set contentFloor to (bandBottom + columnHeight + gap) + + -- Center the column vertically in that space, and center each button horizontally + -- on the dialog's mid-line. buttonViews is in reverse on-screen order (added in + -- reverse), so placing item 1 at the bottom makes the visible top-to-bottom order + -- match buttonList. Each button keeps its own fitted width so the centering + -- survives the relayout NSAlert performs while presenting the dialog. + set regionTop to contentFloor - gap + set columnBottom to bandBottom + if (regionTop - bandBottom) > columnHeight then set columnBottom to bandBottom + (((regionTop - bandBottom) - columnHeight) / 2) + set centerX to contentWidth / 2 + set maxButtonWidth to contentWidth - (sideMargin * 2) + repeat with buttonIndex from 1 to buttonCount + set theButton to item buttonIndex of buttonViews + theButton's sizeToFit() + set fittedWidth to (item 1 of item 2 of (theButton's frame())) as real + if fittedWidth > maxButtonWidth then set fittedWidth to maxButtonWidth + set buttonX to centerX - (fittedWidth / 2) + if buttonX is less than sideMargin then set buttonX to sideMargin + set buttonY to columnBottom + ((buttonIndex - 1) * (buttonHeight + gap)) + theButton's setFrame:(current application's NSMakeRect(buttonX, buttonY, fittedWidth, buttonHeight)) + theButton's setAutoresizingMask:0 + end repeat +end stackAlertButtonsVertically + on setAlertIcon(alert, theIconPath) if theIconPath is "" then return From 96d55c2e3dfdd7b5f2848fc47bef3e02e66e866f Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 22 Jun 2026 22:44:40 +0900 Subject: [PATCH 2/7] Harden vertical button layout against edge-case content views - Guard buttonContainer against missing value before removeFromSuperview so a button with no superview can never trigger a runtime error. - Capture the content view height and skip any near-full-height background or visual-effect view when computing the content floor, so a full-span view (present on some macOS versions) cannot drag the floor to 0 and collapse the vertical centering. Hairline dividers are kept as a valid floor. --- src/agent_hooks/assets/dialog.applescript | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/agent_hooks/assets/dialog.applescript b/src/agent_hooks/assets/dialog.applescript index 08495be..afba3bd 100644 --- a/src/agent_hooks/assets/dialog.applescript +++ b/src/agent_hooks/assets/dialog.applescript @@ -51,7 +51,9 @@ on stackAlertButtonsVertically(alert) set theWindow to alert's |window|() set contentView to theWindow's contentView() - set contentWidth to (item 1 of item 2 of (contentView's frame())) as real + set contentViewFrame to contentView's frame() + set contentWidth to (item 1 of item 2 of contentViewFrame) as real + set contentHeight to (item 2 of item 2 of contentViewFrame) as real set gap to 8 set sideMargin to 20 @@ -79,7 +81,7 @@ on stackAlertButtonsVertically(alert) -- Reparent the buttons directly onto the content view so they can be positioned -- in absolute coordinates, then drop the now-empty native button container. set buttonContainer to (item 1 of buttonViews)'s superview() - if buttonContainer is not contentView then + if buttonContainer is not missing value and buttonContainer is not contentView then repeat with theButton in buttonViews theButton's removeFromSuperview() contentView's addSubview:theButton @@ -100,12 +102,17 @@ on stackAlertButtonsVertically(alert) -- Find the lowest piece of content (the divider NSAlert draws above the buttons, -- or the message text) so the column can be centered in the space between it and - -- the bottom of the dialog. + -- the bottom of the dialog. Skip any near-full-height background or visual-effect + -- view some macOS versions add: its bottom sits at 0, which would drag the floor + -- down and collapse the centering. set contentFloor to missing value repeat with childView in (contentView's subviews()) if not ((childView's isKindOfClass:(current application's NSButton)) as boolean) then - set childBottom to (item 2 of item 1 of (childView's frame())) as real - if contentFloor is missing value or childBottom < contentFloor then set contentFloor to childBottom + set childFrame to childView's frame() + if ((item 2 of item 2 of childFrame) as real) < (contentHeight - 5) then + set childBottom to (item 2 of item 1 of childFrame) as real + if contentFloor is missing value or childBottom < contentFloor then set contentFloor to childBottom + end if end if end repeat if contentFloor is missing value then set contentFloor to (bandBottom + columnHeight + gap) From b700d69df939d0c4a12796d87a69cb7361e9e784 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 22 Jun 2026 22:49:15 +0900 Subject: [PATCH 3/7] Handle NSVisualEffectView-wrapped alert content on modern macOS - Exclude near-full-height views from the top-pinning autoresize pass so a full-height background/visual-effect view keeps resizing and fills the grown window instead of leaving a blank strip. - When NSAlert nests the icon and text inside an NSVisualEffectView, descend into that container and measure the content floor in content-view coordinates, so the wrapper itself no longer collapses the floor to 0 and the column stays vertically centered. --- src/agent_hooks/assets/dialog.applescript | 31 ++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/agent_hooks/assets/dialog.applescript b/src/agent_hooks/assets/dialog.applescript index afba3bd..fd605ec 100644 --- a/src/agent_hooks/assets/dialog.applescript +++ b/src/agent_hooks/assets/dialog.applescript @@ -90,10 +90,14 @@ on stackAlertButtonsVertically(alert) end if -- Pin the existing content to the top so growing the window adds room at the - -- bottom for the taller column rather than stretching the message area. + -- bottom for the taller column rather than stretching the message area. Leave any + -- near-full-height background or visual-effect view with its own mask so it keeps + -- resizing to fill the grown window instead of leaving a blank strip at the bottom. if extraHeight is greater than 0 then repeat with childView in (contentView's subviews()) - childView's setAutoresizingMask:8 -- NSViewMinYMargin: stay anchored to the top + if ((item 2 of item 2 of (childView's frame())) as real) < (contentHeight - 5) then + childView's setAutoresizingMask:8 -- NSViewMinYMargin: stay anchored to the top + end if end repeat set windowFrame to theWindow's frame() set grownFrame to current application's NSMakeRect((item 1 of item 1 of windowFrame), ((item 2 of item 1 of windowFrame) - extraHeight), (item 1 of item 2 of windowFrame), ((item 2 of item 2 of windowFrame) + extraHeight)) @@ -102,15 +106,24 @@ on stackAlertButtonsVertically(alert) -- Find the lowest piece of content (the divider NSAlert draws above the buttons, -- or the message text) so the column can be centered in the space between it and - -- the bottom of the dialog. Skip any near-full-height background or visual-effect - -- view some macOS versions add: its bottom sits at 0, which would drag the floor - -- down and collapse the centering. - set contentFloor to missing value + -- the bottom of the dialog. On some macOS versions NSAlert nests the icon and text + -- inside an NSVisualEffectView, so descend into that container when present and + -- measure in content-view coordinates. Still skip any near-full-height view: a + -- background or effect view whose bottom sits at 0 would otherwise collapse the + -- centering. + set floorContainer to contentView repeat with childView in (contentView's subviews()) + if ((childView's isKindOfClass:(current application's NSVisualEffectView)) as boolean) then + set floorContainer to childView + exit repeat + end if + end repeat + set contentFloor to missing value + repeat with childView in (floorContainer's subviews()) if not ((childView's isKindOfClass:(current application's NSButton)) as boolean) then - set childFrame to childView's frame() - if ((item 2 of item 2 of childFrame) as real) < (contentHeight - 5) then - set childBottom to (item 2 of item 1 of childFrame) as real + set childRect to floorContainer's convertRect:(childView's frame()) toView:contentView + if ((item 2 of item 2 of childRect) as real) < (contentHeight - 5) then + set childBottom to (item 2 of item 1 of childRect) as real if contentFloor is missing value or childBottom < contentFloor then set contentFloor to childBottom end if end if From 18f8ca0254548557e90d7a55a4bd5d8b7250dd67 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 22 Jun 2026 22:51:14 +0900 Subject: [PATCH 4/7] Coerce alert's buttons() to a native list alert's buttons() returns an NSArray; coerce it to an AppleScript list so the count and item-index operations in stackAlertButtonsVertically are reliable across AppleScript-ObjC versions. --- src/agent_hooks/assets/dialog.applescript | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/agent_hooks/assets/dialog.applescript b/src/agent_hooks/assets/dialog.applescript index fd605ec..03b1525 100644 --- a/src/agent_hooks/assets/dialog.applescript +++ b/src/agent_hooks/assets/dialog.applescript @@ -45,7 +45,9 @@ on stackAlertButtonsVertically(alert) -- packs buttons into a horizontal row by default, which overlaps once the dialog -- is widened to fit a long message and the larger dialog font. Stacking top to -- bottom removes that failure mode regardless of dialog width or button count. - set buttonViews to alert's buttons() + -- Coerce the returned NSArray to a native AppleScript list so the index/count + -- operations below are reliable across AppleScript-ObjC versions. + set buttonViews to (alert's buttons()) as list set buttonCount to (count of buttonViews) if buttonCount is less than 2 then return From 57debdd6d1eab3149c92759dbcf3178740b028e8 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 22 Jun 2026 22:54:17 +0900 Subject: [PATCH 5/7] Preserve autoresize flags and target only the alert's action buttons - When anchoring content to the top, add the NSViewMinYMargin flag to each view's existing mask instead of overwriting it, so flags such as NSViewWidthSizable are retained. - Use the alert's buttons() NSArray with containsObject: to exclude only the alert's own action buttons from the content-floor scan, so any other buttons (e.g. a help button or accessory checkboxes) still count as content. --- src/agent_hooks/assets/dialog.applescript | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/agent_hooks/assets/dialog.applescript b/src/agent_hooks/assets/dialog.applescript index 03b1525..8175166 100644 --- a/src/agent_hooks/assets/dialog.applescript +++ b/src/agent_hooks/assets/dialog.applescript @@ -45,9 +45,10 @@ on stackAlertButtonsVertically(alert) -- packs buttons into a horizontal row by default, which overlaps once the dialog -- is widened to fit a long message and the larger dialog font. Stacking top to -- bottom removes that failure mode regardless of dialog width or button count. - -- Coerce the returned NSArray to a native AppleScript list so the index/count - -- operations below are reliable across AppleScript-ObjC versions. - set buttonViews to (alert's buttons()) as list + -- Keep the raw NSArray for membership tests and coerce a native AppleScript list + -- for the index/count operations below (reliable across AppleScript-ObjC versions). + set buttonArray to alert's buttons() + set buttonViews to buttonArray as list set buttonCount to (count of buttonViews) if buttonCount is less than 2 then return @@ -98,7 +99,10 @@ on stackAlertButtonsVertically(alert) if extraHeight is greater than 0 then repeat with childView in (contentView's subviews()) if ((item 2 of item 2 of (childView's frame())) as real) < (contentHeight - 5) then - childView's setAutoresizingMask:8 -- NSViewMinYMargin: stay anchored to the top + -- Add NSViewMinYMargin (8) so the view stays anchored to the top, + -- without dropping its other flags (e.g. NSViewWidthSizable). + set currentMask to (childView's autoresizingMask()) as integer + if (currentMask div 8) mod 2 is 0 then childView's setAutoresizingMask:(currentMask + 8) end if end repeat set windowFrame to theWindow's frame() @@ -122,7 +126,7 @@ on stackAlertButtonsVertically(alert) end repeat set contentFloor to missing value repeat with childView in (floorContainer's subviews()) - if not ((childView's isKindOfClass:(current application's NSButton)) as boolean) then + if not ((buttonArray's containsObject:childView) as boolean) then set childRect to floorContainer's convertRect:(childView's frame()) toView:contentView if ((item 2 of item 2 of childRect) as real) < (contentHeight - 5) then set childBottom to (item 2 of item 1 of childRect) as real From b9e62b43e150aa2dd5b5ba79a591bc595c7e1db7 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 22 Jun 2026 23:18:23 +0900 Subject: [PATCH 6/7] Use isEqual: and dereference loop variables in the button layout - Compare the button container against the content view with isEqual: instead of the AppleScript identity operator, which is unreliable for ObjC object wrappers and could otherwise remove the content view and blank the dialog. - Dereference each repeat-loop variable with 'contents of' before using it, so the underlying NSView is passed to ObjC methods (notably addSubview:) rather than an AppleScript reference wrapper. --- src/agent_hooks/assets/dialog.applescript | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/agent_hooks/assets/dialog.applescript b/src/agent_hooks/assets/dialog.applescript index 8175166..3554c84 100644 --- a/src/agent_hooks/assets/dialog.applescript +++ b/src/agent_hooks/assets/dialog.applescript @@ -66,7 +66,8 @@ on stackAlertButtonsVertically(alert) set bandBottom to missing value set bandTop to missing value set buttonHeight to 0 - repeat with theButton in buttonViews + repeat with theButtonRef in buttonViews + set theButton to contents of theButtonRef theButton's sizeToFit() set bandRect to (theButton's superview())'s convertRect:(theButton's frame()) toView:contentView set rectBottom to (item 2 of item 1 of bandRect) as real @@ -84,8 +85,9 @@ on stackAlertButtonsVertically(alert) -- Reparent the buttons directly onto the content view so they can be positioned -- in absolute coordinates, then drop the now-empty native button container. set buttonContainer to (item 1 of buttonViews)'s superview() - if buttonContainer is not missing value and buttonContainer is not contentView then - repeat with theButton in buttonViews + if buttonContainer is not missing value and not (buttonContainer's isEqual:contentView) then + repeat with theButtonRef in buttonViews + set theButton to contents of theButtonRef theButton's removeFromSuperview() contentView's addSubview:theButton end repeat @@ -97,7 +99,8 @@ on stackAlertButtonsVertically(alert) -- near-full-height background or visual-effect view with its own mask so it keeps -- resizing to fill the grown window instead of leaving a blank strip at the bottom. if extraHeight is greater than 0 then - repeat with childView in (contentView's subviews()) + repeat with childViewRef in (contentView's subviews()) + set childView to contents of childViewRef if ((item 2 of item 2 of (childView's frame())) as real) < (contentHeight - 5) then -- Add NSViewMinYMargin (8) so the view stays anchored to the top, -- without dropping its other flags (e.g. NSViewWidthSizable). @@ -118,14 +121,16 @@ on stackAlertButtonsVertically(alert) -- background or effect view whose bottom sits at 0 would otherwise collapse the -- centering. set floorContainer to contentView - repeat with childView in (contentView's subviews()) + repeat with childViewRef in (contentView's subviews()) + set childView to contents of childViewRef if ((childView's isKindOfClass:(current application's NSVisualEffectView)) as boolean) then set floorContainer to childView exit repeat end if end repeat set contentFloor to missing value - repeat with childView in (floorContainer's subviews()) + repeat with childViewRef in (floorContainer's subviews()) + set childView to contents of childViewRef if not ((buttonArray's containsObject:childView) as boolean) then set childRect to floorContainer's convertRect:(childView's frame()) toView:contentView if ((item 2 of item 2 of childRect) as real) < (contentHeight - 5) then From af6b2aa64f9153378b84fbd5dde1e8647390bce7 Mon Sep 17 00:00:00 2001 From: LIU ZHE YOU Date: Mon, 22 Jun 2026 23:23:30 +0900 Subject: [PATCH 7/7] Scan both content-view and effect-view subviews for the content floor Gather the content view's own subviews together with any NSVisualEffectView container's subviews into a single candidate list, measuring each via its own superview, so a direct accessory view is no longer ignored when the icon/text are nested inside an effect view. Keeps isKindOfClass: for the container check (subclass-aware, unlike a className string match). --- src/agent_hooks/assets/dialog.applescript | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/agent_hooks/assets/dialog.applescript b/src/agent_hooks/assets/dialog.applescript index 3554c84..a943b1e 100644 --- a/src/agent_hooks/assets/dialog.applescript +++ b/src/agent_hooks/assets/dialog.applescript @@ -115,24 +115,24 @@ on stackAlertButtonsVertically(alert) -- Find the lowest piece of content (the divider NSAlert draws above the buttons, -- or the message text) so the column can be centered in the space between it and - -- the bottom of the dialog. On some macOS versions NSAlert nests the icon and text - -- inside an NSVisualEffectView, so descend into that container when present and - -- measure in content-view coordinates. Still skip any near-full-height view: a - -- background or effect view whose bottom sits at 0 would otherwise collapse the - -- centering. - set floorContainer to contentView + -- the bottom of the dialog. Gather the content view's own subviews and, when + -- NSAlert nests the icon/text inside an NSVisualEffectView, that container's + -- subviews too, so neither a direct accessory view nor the nested content is + -- missed. Each candidate is measured in content-view coordinates via its own + -- superview. Skip the action buttons and any near-full-height background/effect + -- view whose bottom sits at 0, which would otherwise collapse the centering. + set candidateViews to (contentView's subviews()) as list repeat with childViewRef in (contentView's subviews()) set childView to contents of childViewRef if ((childView's isKindOfClass:(current application's NSVisualEffectView)) as boolean) then - set floorContainer to childView - exit repeat + set candidateViews to candidateViews & ((childView's subviews()) as list) end if end repeat set contentFloor to missing value - repeat with childViewRef in (floorContainer's subviews()) + repeat with childViewRef in candidateViews set childView to contents of childViewRef if not ((buttonArray's containsObject:childView) as boolean) then - set childRect to floorContainer's convertRect:(childView's frame()) toView:contentView + set childRect to (childView's superview())'s convertRect:(childView's frame()) toView:contentView if ((item 2 of item 2 of childRect) as real) < (contentHeight - 5) then set childBottom to (item 2 of item 1 of childRect) as real if contentFloor is missing value or childBottom < contentFloor then set contentFloor to childBottom