Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dist/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const patches = [
"peekaboo-minimized-close-test.patch",
"peekaboo-notification-actions.patch",
"peekaboo-fullscreen.patch",
"peekaboo-date-value.patch",
].map((name) => join(native, "patches", name));
function spawn(command, env) {
return spawnSync(command[0], command.slice(1), { encoding: "utf8", env: env ?? process.env });
Expand Down
18 changes: 15 additions & 3 deletions dist/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { failureAvailability, windowAvailability } from "./availability.js";
import { canonicalDate } from "./date-value.js";
import { DESKTOP_BUTTONS, DESKTOP_CLICKS, DESKTOP_DIRECTIONS, DESKTOP_ELEMENT_ACTIONS, DESKTOP_MODIFIERS, DESKTOP_SELECTIONS, isDesktopAction, isDesktopKey, isDesktopManagement, } from "./protocol.js";
import { NOTIFICATIONS_MS, notificationAction, readNotifications, validateNotificationAction, } from "./notifications.js";
import { waitFor } from "./wait.js";
Expand Down Expand Up @@ -674,8 +675,11 @@ function validateAction(request) {
throw new Error(`Choose one of ${DESKTOP_ELEMENT_ACTIONS.join(", ")}.`);
}
if (request.op === "set-value" && typeof request.value !== "boolean"
&& (typeof request.value !== "number" || !Number.isFinite(request.value))) {
throw new Error("Set a finite number or true/false; use desktop_type for text.");
&& (typeof request.value !== "number" || !Number.isFinite(request.value))
&& (typeof request.value !== "string" || canonicalDate(request.value) === undefined)) {
throw new Error(typeof request.value === "string"
? "Set a date as ISO 8601: 2026-10-17, 2026-10-17T15:30 (local time) or 2026-10-17T15:30:00Z. Use desktop_type for text."
: "Set a finite number, true/false or an ISO 8601 date; use desktop_type for text.");
}
if (request.op === "type" && (typeof request.text !== "string" || request.text.length > 8192)) {
throw new Error("Replacement text must contain at most 8192 UTF-16 code units.");
Expand Down Expand Up @@ -876,7 +880,9 @@ async function act(native, request, signal) {
format: request.format,
image: await clipboardImageInput(request.path, signal),
}
: request;
: request.op === "set-value" && typeof request.value === "string"
? { ...request, value: canonicalDate(request.value) }
: request;
const operation = request.op === "menu"
? "menu"
: request.op === "dialog-answer"
Expand Down Expand Up @@ -924,6 +930,12 @@ async function act(native, request, signal) {
}, outcome);
}
const outcome = data.outcome;
if (request.op === "set-value" && typeof request.value === "string" && outcome !== "completed") {
data = {
...data,
keyboard_route: "If the date control refuses Accessibility, click its first field (such as the month) with desktop_click, type its digits with desktop_key, and press Tab to move to the next field, or Up and Down to step the focused field; then inspect to read the value back.",
};
}
if (request.op === "menu") {
const receipt = data.target_receipt;
if ((outcome === "completed" || receipt) && (!receipt || receipt.window_id !== undefined
Expand Down
5 changes: 5 additions & 0 deletions dist/date-value.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Reads an ISO 8601 date or date-time as the UTC instant a date control stores, to the second.
* A date alone is local midnight and a time without an offset is local, as a date picker shows them.
*/
export declare function canonicalDate(text: string): string | undefined;
31 changes: 31 additions & 0 deletions dist/date-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const pattern = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})?)?$/;
/**
* Reads an ISO 8601 date or date-time as the UTC instant a date control stores, to the second.
* A date alone is local midnight and a time without an offset is local, as a date picker shows them.
*/
export function canonicalDate(text) {
const match = pattern.exec(text);
if (!match)
return undefined;
const [year, month, day, hour, minute, second] = match.slice(1, 7).map((part) => Number(part ?? 0));
const zone = match[7];
if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59)
return undefined;
// Rejects days past the month's end, which Date would roll into the next month.
const days = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > days)
return undefined;
let time;
if (zone === undefined) {
time = new Date(year, month - 1, day, hour, minute, second).getTime();
}
else {
const offset = zone === "Z"
? 0
: (zone[0] === "-" ? -1 : 1) * (Number(zone.slice(1, 3)) * 60 + Number(zone.slice(4, 6)));
time = Date.UTC(year, month - 1, day, hour, minute, second) - offset * 60_000;
}
if (!Number.isFinite(time))
return undefined;
return new Date(time).toISOString().replace(/\.\d{3}Z$/, "Z");
}
2 changes: 1 addition & 1 deletion dist/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export type DesktopRequest = DesktopManagement | DesktopLaunch | DesktopOpen | D
op: "set-value";
snapshot: string;
element: string;
value: number | boolean;
value: number | boolean | string;
} | {
op: "select";
snapshot: string;
Expand Down
4 changes: 2 additions & 2 deletions dist/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -690,14 +690,14 @@ Optional modifiers (command, control, option and shift, each at most once) are h
{
name: "desktop_set_value",
op: "set-value",
description: `Set the value of one non-text control from a fresh desktop_inspect result and verify it by reading the same element back: a number for a slider, stepper (AXIncrementor), or level indicator, or true/false for a checkbox, switch, or disclosure triangle (true expands it). Pass snapshot_id as snapshot and the literal element ID as element. A number is written directly and must read back exactly, so a control that rounds or clamps it reports unknown; desktop_element_action with AXIncrement or AXDecrement steps a control without a settable value. A toggle with a read-only value that advertises AXPress gets at most one press, and none when it already has the requested state; a mixed state is refused. Use desktop_type for text fields. A pop-up button has no settable value: open it with desktop_click, then choose the item from the fresh observation, which lists an open menu's items first, with desktop_element_action AXPick or desktop_click. The snapshot binds the exact process, window, and element and is single-use. This does not activate the window. Inspect again after an unknown result and never blindly repeat interrupted input.`,
description: `Set the value of one non-text control from a fresh desktop_inspect result and verify it by reading the same element back: a number for a slider, stepper (AXIncrementor), or level indicator; true/false for a checkbox, switch, or disclosure triangle (true expands it); or an ISO 8601 date for a date or time picker (AXDateTimeArea, AXDateField or AXTimeField): 2026-10-17 is local midnight, 2026-10-17T15:30 is local time, and Z or an offset such as +02:00 names the zone. A date is written as the control's date value and must read back to the second; the result shows it in UTC, and a control that drops the time or seconds reports unknown. If a date control refuses, the result's keyboard_route explains entering it field by field. Pass snapshot_id as snapshot and the literal element ID as element. A number is written directly and must read back exactly, so a control that rounds or clamps it reports unknown; desktop_element_action with AXIncrement or AXDecrement steps a control without a settable value. A toggle with a read-only value that advertises AXPress gets at most one press, and none when it already has the requested state; a mixed state is refused. Use desktop_type for text fields. A pop-up button has no settable value: open it with desktop_click, then choose the item from the fresh observation, which lists an open menu's items first, with desktop_element_action AXPick or desktop_click. The snapshot binds the exact process, window, and element and is single-use. This does not activate the window. Inspect again after an unknown result and never blindly repeat interrupted input.`,
parameters: {
type: "object",
required: ["snapshot", "element", "value"],
properties: {
snapshot: id,
element: id,
value: { anyOf: [{ type: "number" }, { type: "boolean" }] },
value: { anyOf: [{ type: "number" }, { type: "boolean" }, { type: "string", maxLength: 64 }] },
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ A status item is pressed through Accessibility in its owner process without movi
| `desktop_press` | Press and hold an element or point for up to 10 seconds, then release in place |
| `desktop_type` | Replace an editable field's whole value |
| `desktop_select` | Select literal text in a field, or put the caret before or after it |
| `desktop_set_value` | Set a slider, stepper or level indicator to a number, or a checkbox, switch or disclosure triangle on or off, and read it back |
| `desktop_set_value` | Set a slider, stepper or level indicator to a number, a checkbox, switch or disclosure triangle on or off, or a date or time picker to an ISO 8601 date, and read it back |
| `desktop_element_action` | Perform one Accessibility action an element lists, such as `AXShowMenu`, `AXIncrement`, `AXDecrement`, `AXPick` or `AXCancel` |
| `desktop_insert` | Insert text at the caret or over the selection with one temporary paste, then restore the clipboard; where no text control can be observed, type it into the window's keyboard focus |
| `desktop_key` | Press one key with optional Command, Control, Option and Shift: any character key, such as `,` or `/`, or a named key |
Expand Down
12 changes: 12 additions & 0 deletions native/patches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,18 @@ exact window, and a signed confirmed response must carry the requested state. Te
rules and the signed postcondition for both states; the operation inventory counts rise to 128.


`peekaboo-date-value.patch` lets set-value write a date. Date and time controls such as `NSDatePicker` expose an
`NSDate` as `AXValue`, which upstream `setValue` cannot read back (`ElementValueReadback` has no date) and
would overwrite with a string. With the patch, when the element's current value is a date and the request is
a string, the driver takes a date path: the string must be UTC ISO 8601 to the second
(`2026-10-17T15:30:00Z`, the one spelling `ElementDateValue` formats), anything else is refused as
`valueNotSettable` before dispatch, and an equal current value completes with no change. Otherwise it writes
an `NSDate` through the new `setAutomationDate`, polls the value for up to one second, and confirms only when
it reads back the same string; else the outcome is the usual unverified value mutation. The verification
reports the readback as a string in the same spelling, so the existing signed set-value binding, which
compares the readback with the requested string, applies unchanged. Other values and elements take the
existing path. Tests cover the write, the no-change case, refused spellings and a mismatched readback.

When updating Peekaboo, review the upstream fixes and these patches together. Update the revision
guard and patches deliberately, or remove each patch when the dependency includes its fix.
Revalidate real button clicks, changed-focus refusal, literal multiline insertion, clipboard
Expand Down
Loading
Loading