Skip to content

feat: choose a path in an exact Open or Save panel - #57

Merged
iamnbutler merged 1 commit into
mainfrom
feat/dialog-path
Oct 17, 2026
Merged

iamnbutler merged 1 commit into
mainfrom
feat/dialog-path

Conversation

@iamnbutler

@iamnbutler iamnbutler commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Adds path to desktop_dialog_answer, so an agent can choose a folder or file in an Open or Save panel. Before this, a save panel's file name field could be typed into, but a panel couldn't be pointed at a folder. #38 left this unsupported because Peekaboo's dialogHandleFile only finds the panel by fuzzy app name.

Behavior

desktop_dialog_answer { target, path } takes an absolute path and leaves the default button unpressed:

  • An existing folder becomes the panel's folder.
  • Any other path must name a file inside an existing folder:
    • A Save panel takes that folder and the file name.
    • An Open panel selects the file through Go to Folder.
  • A path inside a missing folder is refused before any input.

Pressing Save or Open is still a separate button answer, so the agent can list the dialog and check it first.

This is foreground input, which the lead approved where background delivery can't work. The tool description and docs say so:

  • The host activates the panel's app.
  • Before every key, it checks that the exact retained panel has keyboard focus.

Outcome:

  • completed only when the panel reads back the folder and, for a Save panel, the name.
  • Otherwise unknown, with DESKTOP_ACTION_UNVERIFIED and a hint to list the dialog before pressing its default button. This covers a file chosen in an Open panel, since there is no selection readback, and a panel that exposes no folder.
  • The readback values are in details: directory_readback, filename_readback, path_navigation_method and panel.

Native

New patch peekaboo-dialog-path.patch (last in the stack) adds the exact Bridge operation exactFileDialogPath(FileDialogPathRequest { target: DialogTargetSelector, path, focus }).

How the host resolves the panel:

  • It resolves the panel from the app, PID or window selector, the same way exact forced dismissal does.
  • It refuses non-file dialogs.
  • It never clicks the panel, unlike upstream's center click.

How it enters the path:

  1. Expand a collapsed panel.
  2. If the expanded panel has a PathTextField, set it through Accessibility and press Return.
  3. Otherwise, or when that field doesn't read back the folder, use Go to Folder (Command-Shift-G). The Go To field is set through Accessibility when one new settable field appears; otherwise the path is typed.
  4. For a Save panel, set the name in saveAsNameTextField.

Paths resolve symlinks before input, so /tmp compares as /private/tmp.

Result: confirmedChange when the readback matches, otherwise dispatchedUnverified. The descriptor is service-owned:

  • Permissions: Accessibility and PostEvent.
  • Response: responseResolved dialog result.
  • Delivery: global or composite foreground, with a variable unit count.

Wiring: request and response, descriptor, permission partition, semantic policy, result contract, signed dialog-result receipt validation, server handler and client. The handler casts to a new FileDialogPathProviding protocol, as menuCommand does, so upstream test doubles don't change.

Client:

  • dialog.swift requires the operation in the handshake and checks the dialog is a file panel on a fresh read.
  • It verifies the signed receipt's operation, outcome and window.
  • The runtime's allowedOperations gains .exactFileDialogPath.

Tests

  • npm run check
  • Signed native build on the full stack (node dist/app.js … --identity "Developer ID Application: Nathan Butler (8S43JJMN6B)").
  • PeekabooAutomationKit (foreground, narrow filter, hard kill): new DialogFileDialogPathTests (folder, file in a symlinked folder, missing folder, request validation and decode revalidation), plus DialogExactInputContractTests and DialogForegroundOutcomeTests. 37 tests passed.
  • Rebased on main after fix: wait out a quarantined ScreenCaptureKit gate and explain the refusal #63 (b8d7a4e); peekaboo-dialog-path.patch is last. Following fix: update upstream operation inventory tests in each patch that adds an operation #60's scheme, the patch sets the wire count to 127 (main is 126) and adds .exactFileDialogPath to the accessibilityAndPostEvent partition and the mutating-operation list. All 95 PeekabooTests Bridge suites, run narrowly in the foreground in three batches: everything passes except the known failures. PeekabooBridgeWindowEnumerationTimeoutTests and PeekabooBridgeSetValueFailureTests fail under batch load and pass alone. "minimized identity may dispatch when WindowServer omits its entry" is Bridge test 'minimized identity may dispatch when WindowServer omits its entry' fails on main #50 and fails on main too; it depends on the host. AutomationKit DialogFileDialogPathTests and DialogService suites: 10 tests pass. npm run check passes.
  • Live, dev app:
    • A path answer reaches the native client and passes the handshake check, then is refused because the session is locked.
    • A relative path, or path together with button, is refused in TypeScript.

Pending: screen locked

The GUI session is locked (IOConsoleLocked), so no panel could be opened or answered. Exact checks to run with the dev app (open -n --env DESKTOP_TOOLS_STATE=/tmp/<x> "Desktop Tools.app") and TextEdit:

  1. Save panel, collapsed:
    • Open TextEdit's Save panel (Command-S on a new document). Then desktop_dialog on the window: expect is_file_dialog: true.
    • desktop_dialog_answer with path: "/tmp/dt-path-test/Report.rtf", after mkdir /tmp/dt-path-test.
    • Expect completed, the panel expanded to /private/tmp/dt-path-test, and the name field Report.rtf.
    • Then answer button: "Save" and check the file exists.
  2. Save panel, folder only: path: "/Users/<you>/Desktop". Expect completed, the folder changed, and the name unchanged.
  3. Open panel:
    • In TextEdit, Command-O, then path naming an existing file.
    • Expect unknown with DESKTOP_ACTION_UNVERIFIED and the file selected in the panel.
    • Then button: "Open" opens it.
  4. Missing folder: path: "/tmp/does-not-exist/x.rtf" is refused with no input sent.
  5. Not a file panel: a "Do you want to save" alert (close an edited document) is refused for path.
  6. Focus lost: with the Save panel open, start the answer and switch apps (or open another window over it) mid-navigation. Expect no keystrokes in the other app, and unknown or refused.
  7. Readback detail: on a panel where the expanded PathTextField is missing (if any), check that path_navigation_method is go_to_folder and the outcome is unknown rather than completed.

@iamnbutler
iamnbutler merged commit 4f02961 into main Oct 17, 2026
2 checks passed
@iamnbutler
iamnbutler deleted the feat/dialog-path branch October 17, 2026 04:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant