Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1198e28
improvement(computer-use): remove the activity strip above the chat c…
waleedlatif1 Nov 13, 2026
96c4b0b
fix(aws-marketplace): check the container license's uncounted usage e…
waleedlatif1 Nov 13, 2026
cd82e4f
feat(computer-use): batch 2-8 actions per call and read app state in …
waleedlatif1 Nov 13, 2026
afb6653
improvement(computer-use): fold empty layout groups and compact the a…
waleedlatif1 Nov 13, 2026
e368663
docs(library): update best-ai-agent-marketplaces-template-libraries (…
waleedlatif1 Nov 13, 2026
849a668
docs(library): update best-ai-agents-for-customer-support-automation …
waleedlatif1 Nov 13, 2026
5bfef3b
docs(library): update open-source-ai-agent-platforms (#10001)
waleedlatif1 Nov 13, 2026
d3ce5ce
docs(library): update best-ai-agents-invoice-processing-finance-recon…
waleedlatif1 Nov 13, 2026
86817f3
docs(library): update best-ai-agent-platforms-2026 (#10002)
waleedlatif1 Nov 13, 2026
0f9009c
improvement(search-mcp): log the field names a rejected tool call's a…
waleedlatif1 Nov 13, 2026
6cb5bfc
fix(computer-use): click an editor into focus when Accessibility cann…
waleedlatif1 Nov 13, 2026
aa0ff21
fix(hex,stripe): validate Hex and Stripe against their API docs (#10005)
waleedlatif1 Nov 13, 2026
07330ae
fix(blocks): keep agent-supplied tool args that block mappers remap f…
waleedlatif1 Nov 13, 2026
7f47f4b
fix(google-drive): keep the page token an agent passes (#10008)
waleedlatif1 Nov 13, 2026
823f9f1
improvement(search-mcp): log why a batched read returned an error for…
waleedlatif1 Nov 13, 2026
a944b97
improvement(search): name matching Slack channels and say whether a f…
waleedlatif1 Nov 13, 2026
30f6b8e
improvement(blocks): share the tool-call fallback helper and tidy the…
waleedlatif1 Nov 13, 2026
db39bbb
feat(monaco): add Monaco CRM integration (#10009)
waleedlatif1 Nov 13, 2026
17a38b3
improvement(credentials): hold unsaved keyless subjects so trust poli…
waleedlatif1 Nov 13, 2026
06f077d
fix(computer-use): tighten batch and click-to-focus edge cases from t…
waleedlatif1 Nov 13, 2026
d9af98b
fix(desktop-browser): refuse typing behind an unmarked full-screen mo…
waleedlatif1 Nov 13, 2026
b5b8ec3
fix(slack): let Block Kit messages carry notification text (#10018)
waleedlatif1 Nov 13, 2026
68f4c69
fix(monaco): reject a stage key and a stage ID together on opportunit…
waleedlatif1 Nov 13, 2026
3caa269
fix(credentials): cap the Databricks identity check with a deadline a…
waleedlatif1 Nov 13, 2026
9cad5ff
improvement(search): skip the Slack channel lookup on channel-scoped …
waleedlatif1 Nov 13, 2026
cba987c
fix(hex): type the block projectVersion output as any and mark catego…
waleedlatif1 Nov 13, 2026
e139602
chore(credentials): move the IAM refusal TSDoc back onto iamFailureMe…
waleedlatif1 Nov 13, 2026
377e81b
fix(computer-use): type after a focus click only once the caret is co…
waleedlatif1 Nov 14, 2026
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
4 changes: 2 additions & 2 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,8 @@ Terminal control),
which the helper refuses. With **Ask before using a new
app** on, each app's first use is approved for the task or persistently, and persistent approvals are
listed in Settings and can be revoked. Sign-out and deployment changes clear opt-in, the ask choice
and approvals. The conversation shows the
active app/action and a Stop button; **⌘⇧Esc** also stops active work when the shortcut is available.
and approvals. Stopping the chat response, or **⌘⇧Esc** when the shortcut is available, stops active
work; Settings shows the action in progress.
Stop cancels queued actions, in-flight authorization, approval prompts and the native helper.
Already completed input cannot be undone.

Expand Down
217 changes: 194 additions & 23 deletions apps/desktop/native/computer-use/ComputerUse.swift

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions apps/desktop/native/computer-use/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ Coordinates in action inputs are window-local points; node frames are global scr

AXPress and AXSetValue are direct semantic operations. Keyboard events target the app PID. Cmd+A uses the focused editor's writable accessibility selection range where supported. Coordinate mouse input requires the target app in front; `activate_app` makes that focus change explicit and confirms it. Click/drag/scroll validate the foreground process, unchanged window geometry and the app that receives the point; the topmost window-list entry is not the click target because Notification Center and the Dock keep full-screen click-through windows. A hit on a window's own background counts as that window. Background tabs and minimized windows share bounds with the window in front, so they take no coordinate input. An action that opens UI (AXPress, AXPick, AXShowMenu) and times out while the app shows a sheet, modal or menu (`cannotComplete`) counts as dispatched; any other action that times out fails as uncertain. A menu-item press that leaves the item's own menu open with the app highlighting that item (Calendar on macOS 26) is completed with Return. Sheets, including Go to Folder and out-of-process open and save panels whose contents report no window, count as the window they are attached to. Menu item labels carry their shortcuts (for example ⇧⌘N). Some apps ignore background keyboard events. `dispatched:true,verified:false` means only dispatch succeeded; the agent must observe again to establish the outcome. No foreground activation fallback is hidden inside the helper. Native key codes currently assume the US layout for shortcuts; literal text uses Unicode events. Unsupported shortcuts fail explicitly.

`type_text` and `input_sequence` target a text control (text fields and areas, terminals, combo boxes, date and time pickers, or anything with an editable text selection), or a container whose focused editor already holds keyboard focus, such as a web composer; keystrokes are sent only while focus is in a text input. `input_sequence` executes 1–32 key/text steps against one such element. It validates every step before dispatch and checks the same editor, window, and secure-input ancestry between steps. Enter can continue into another step when the same editor and window retain focus. Actual focus loss, Tab, or navigation shortcuts end the sequence before later input. Explicit `activateFirst: true` activates the target app inside the batch before input; it never enables automatic refocusing between steps. Partial execution reports completed steps and an error; callers must observe before retrying. Every mutation is followed by fresh state under the same authorization and queue entry; `observeAfter` optionally requests a screenshot. Observation failure is reported separately so the mutation is never replayed. The model receives a compact, bounded projection of the native snapshot; omitted nodes are reported explicitly.
`type_text` and `input_sequence` target a text control (text fields and areas, terminals, combo boxes, date and time pickers, or anything with an editable text selection), or a container whose focused editor already holds keyboard focus, such as a web composer; keystrokes are sent only while focus is in a text input. When an app ignores the Accessibility focus request (SwiftUI sheets such as Clock's city search), a foreground app's editor is clicked at its center to take focus after 250 ms without it, only if that point hit-tests to the editor (or a non-secure text control inside it) within its observed window; a click refused by the mouse guards sends nothing, the caret is moved to the end and read back so typing inserts there (an empty editor needs no move, and an input_sequence starting with Cmd+A skips the check because it replaces the value); if the caret cannot be confirmed at the end, or focus does not settle, nothing is typed and the call fails with `caret_unconfirmed`. The usual focus checks then apply. `input_sequence` executes 1–32 key/text steps against one such element. It validates every step before dispatch and checks the same editor, window, and secure-input ancestry between steps. Enter can continue into another step when the same editor and window retain focus. Actual focus loss, Tab, or navigation shortcuts end the sequence before later input. Explicit `activateFirst: true` activates the target app inside the sequence before input; it never enables automatic refocusing between steps. Partial execution reports completed steps and an error; callers must observe before retrying. `batch` runs 2–8 click, type_text, set_value, press_key, scroll or perform_action steps planned from one snapshot, each through its own action's checks; it stops at the first step that fails and after any step that changes the app's window structure (on-screen windows, which include sheets, menus and popovers, or the focused window's direct children), reports progress the same way, and a first step that sent nothing is refused with `dispatchState: "not_started"`. Every step is validated before the first one runs. The structure check is a heuristic: it misses modals inside web content and UI that appears after the step's render wait, and stops conservatively on tooltips and autocomplete popovers. Every mutation is followed by fresh state under the same authorization and queue entry; `observeAfter` optionally requests a screenshot. Observation failure is reported separately so the mutation is never replayed. The model receives a compact, bounded projection of the native snapshot; omitted nodes are reported explicitly.

## Cancellation

SIGTERM/SIGINT sets a cancellation flag. Traversal, typing and drag loops check it; mouse up is sent through a defer before returning from a cancelled drag. Keyboard down/up are paired in one function. The owner should close stdin and send SIGTERM, allowing a grace period before SIGKILL. Foreground drags also stop if the active app or window geometry changes; this does not detect every physical user input within the same app. Forced termination cannot guarantee a final release event; input routines deliberately avoid long waits with held buttons. Cancellation cannot undo an action already dispatched.

## Tests and limitations

Compile pure native tests with `swiftc -parse-as-library -D COMPUTER_USE_TEST ComputerUse.swift tests/NativeTests.swift -o native-tests`. They cover multi-display negative origins, coordinate bounds/nonfinite input, shortcut modifiers, invalid key handling and cooperative cancellation. Real status and app discovery outputs were validated against the generated worker Zod schema (156 apps, including 147 installed-only apps).
Compile pure native tests with `swiftc -parse-as-library -D COMPUTER_USE_TEST ComputerUse.swift tests/NativeTests.swift -o native-tests`. They cover multi-display negative origins, coordinate bounds/nonfinite input, shortcut modifiers, invalid key handling, input-sequence and batch validation, protected apps and cooperative cancellation. The live fixture also covers batches and click-to-focus for an editor that ignores Accessibility focus. Real status and app discovery outputs were validated against the generated worker Zod schema (156 apps, including 147 installed-only apps).

`python3 tests/protocol.py <compiled-helper>` checks persistent framing, bounded-request recovery, Unicode decoding, no-prompt status, invalid methods/permissions, permission gates and clean EOF. `tests/Fixture.swift` compiles into a disposable AppKit app with a text field, secure field, increment button/counter, slider, scroll area and a visual-only marker. `python3 tests/live.py <helper>` requires both grants and verifies actual outcomes. `--allow-missing-screen` explicitly reports screenshots blocked rather than passing them; `--existing-fixture` leaves an existing fixture running for coordinated model trials. Give the fixture bundle ID `com.mothership.computer-use-fixture` when packaging it for integration tests.

Expand Down
16 changes: 14 additions & 2 deletions apps/desktop/native/computer-use/tests/Fixture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ final class MarkerView: NSView {
}
}

/// A field that ignores Accessibility focus requests but takes focus from a click, as SwiftUI sheets do.
class ClickFocusField: NSTextField {
override func setAccessibilityFocused(_ accessibilityFocused: Bool) {}
}
/// A click-only editor that also ignores Accessibility caret moves, so its caret cannot be confirmed.
final class FixedCaretField: ClickFocusField {
override func setAccessibilitySelectedTextRange(_ accessibilitySelectedTextRange: NSRange) {}
}
final class FixtureDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var count = 0
Expand All @@ -31,6 +39,9 @@ final class FixtureDelegate: NSObject, NSApplicationDelegate {
let field = NSTextField(string: "Fixture text"); field.setAccessibilityIdentifier("fixture-text"); field.setAccessibilityLabel("Fixture editable text")
let secure = NSSecureTextField(string: "fixture-only-secret"); secure.setAccessibilityIdentifier("fixture-password"); secure.setAccessibilityLabel("Fixture password")
let button = NSButton(title: "Increment", target: self, action: #selector(increment)); button.setAccessibilityIdentifier("fixture-increment")
let clickField = ClickFocusField(string: ""); clickField.setAccessibilityLabel("Fixture click-focus field")
let caretField = FixedCaretField(string: String(repeating: "keep ", count: 40)); caretField.setAccessibilityLabel("Fixture fixed-caret field")
let sheetButton = NSButton(title: "Show sheet", target: self, action: #selector(showSheet)); sheetButton.setAccessibilityIdentifier("fixture-show-sheet")
let slider = NSSlider(value: 25, minValue: 0, maxValue: 100, target: nil, action: nil); slider.setAccessibilityLabel("Fixture slider")
let time = NSDatePicker(); time.datePickerStyle = .textField; time.datePickerElements = .hourMinute; time.setAccessibilityLabel("Fixture time")
time.dateValue = Calendar.current.date(bySettingHour: 10, minute: 15, second: 0, of: Date()) ?? Date(); time.target = self; time.action = #selector(timeChanged(_:))
Expand All @@ -43,15 +54,16 @@ final class FixtureDelegate: NSObject, NSApplicationDelegate {
scroll.documentView = document
scroll.contentView.postsBoundsChangedNotifications = true
NotificationCenter.default.addObserver(self, selector: #selector(scrolled(_:)), name: NSView.boundsDidChangeNotification, object: scroll.contentView)
for view in [field, secure, button, counter, slider, time, timeLabel, marker, scrollCounter, scroll] { stack.addArrangedSubview(view) }
for view in [field, secure, button, counter, clickField, caretField, sheetButton, slider, time, timeLabel, marker, scrollCounter, scroll] { stack.addArrangedSubview(view) }
window.contentView!.addSubview(stack)
NSLayoutConstraint.activate([stack.leadingAnchor.constraint(equalTo: window.contentView!.leadingAnchor, constant: 30), stack.trailingAnchor.constraint(equalTo: window.contentView!.trailingAnchor, constant: -30), stack.topAnchor.constraint(equalTo: window.contentView!.topAnchor, constant: 30), field.widthAnchor.constraint(equalTo: stack.widthAnchor), secure.widthAnchor.constraint(equalTo: stack.widthAnchor), slider.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.heightAnchor.constraint(equalToConstant: 230), marker.widthAnchor.constraint(equalToConstant: 100), marker.heightAnchor.constraint(equalToConstant: 70)])
NSLayoutConstraint.activate([stack.leadingAnchor.constraint(equalTo: window.contentView!.leadingAnchor, constant: 30), stack.trailingAnchor.constraint(equalTo: window.contentView!.trailingAnchor, constant: -30), stack.topAnchor.constraint(equalTo: window.contentView!.topAnchor, constant: 30), field.widthAnchor.constraint(equalTo: stack.widthAnchor), secure.widthAnchor.constraint(equalTo: stack.widthAnchor), clickField.widthAnchor.constraint(equalTo: stack.widthAnchor), caretField.widthAnchor.constraint(equalTo: stack.widthAnchor), slider.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.widthAnchor.constraint(equalTo: stack.widthAnchor), scroll.heightAnchor.constraint(equalToConstant: 230), marker.widthAnchor.constraint(equalToConstant: 100), marker.heightAnchor.constraint(equalToConstant: 70)])
window.makeKeyAndOrderFront(nil)
window.makeFirstResponder(field)
NSApplication.shared.activate()
print("fixture-ready"); fflush(stdout)
}
@objc func increment() { count += 1; counter.stringValue = "Count: \(count)" }
@objc func showSheet() { let alert = NSAlert(); alert.messageText = "Fixture sheet"; alert.addButton(withTitle: "Dismiss"); alert.beginSheetModal(for: window) }
@objc func timeChanged(_ picker: NSDatePicker) {
let time = Calendar.current.dateComponents([.hour, .minute], from: picker.dateValue)
timeLabel.stringValue = "Time: \(time.hour ?? 0):\(String(format: "%02d", time.minute ?? 0))"
Expand Down
24 changes: 18 additions & 6 deletions apps/desktop/native/computer-use/tests/NativeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,25 @@ import AppKit
precondition(bounded.next()?.element == 1)
precondition(bounded.next()?.element == 2)
precondition(bounded.next() == nil)
try validateInputSteps([InputStep(action: "press_key", text: nil, key: "Cmd+A"), InputStep(action: "type_text", text: "hello 🌍", key: nil)])
try validateInputSteps((1...5).flatMap { [InputStep(action: "type_text", text: String($0), key: nil), InputStep(action: "press_key", text: nil, key: "Enter")] })
try validateInputSteps([Parameters(action: "press_key", key: "Cmd+A"), Parameters(action: "type_text", text: "hello 🌍")])
try validateInputSteps((1...5).flatMap { [Parameters(action: "type_text", text: String($0)), Parameters(action: "press_key", key: "Enter")] })
expectError("invalid_arguments") { try validateInputSteps([]) }
expectError("invalid_arguments") { try validateInputSteps(Array(repeating: InputStep(action: "type_text", text: "x", key: nil), count: 33)) }
expectError("invalid_arguments") { try validateInputSteps([InputStep(action: "type_text", text: String(repeating: "🌍", count: 16001), key: nil)]) }
expectError("invalid_arguments") { try validateInputSteps([InputStep(action: "click", text: nil, key: nil)]) }
expectError("unsupported_key") { try validateInputSteps([InputStep(action: "press_key", text: nil, key: "F99")]) }
expectError("invalid_arguments") { try validateInputSteps(Array(repeating: Parameters(action: "type_text", text: "x"), count: 33)) }
expectError("invalid_arguments") { try validateInputSteps([Parameters(action: "type_text", text: String(repeating: "🌍", count: 16001))]) }
expectError("invalid_arguments") { try validateInputSteps([Parameters(action: "click")]) }
expectError("unsupported_key") { try validateInputSteps([Parameters(action: "press_key", key: "F99")]) }
try validateBatchSteps([Parameters(action: "set_value", elementId: "e1", value: "Name"), Parameters(action: "click", elementId: "e2")])
expectError("invalid_arguments") { try validateBatchSteps([Parameters(action: "click", elementId: "e1")]) }
expectError("invalid_arguments") { try validateBatchSteps(Array(repeating: Parameters(action: "press_key", key: "Tab"), count: 9)) }
expectError("invalid_arguments") { try validateBatchSteps([Parameters(action: "drag"), Parameters(action: "press_key", key: "Enter")]) }
expectError("unsupported_key") { try validateBatchSteps([Parameters(action: "click", elementId: "e1"), Parameters(action: "press_key", key: "F99")]) }
expectError("invalid_arguments") { try validateBatchSteps([Parameters(action: "click", elementId: "e1"), Parameters(action: "click")]) }
expectError("invalid_arguments") { try validateBatchSteps([Parameters(action: "click", elementId: "e1"), Parameters(action: "scroll", elementId: "e2")]) }
expectError("invalid_arguments") { try validateBatchSteps([Parameters(action: "click", elementId: "e1"), Parameters(action: "type_text", elementId: "e2")]) }
expectError("invalid_arguments") { try validateBatchSteps([Parameters(action: "set_value", value: "x"), Parameters(action: "click", elementId: "e1")]) }
expectError("invalid_arguments") { try validateBatchSteps([Parameters(action: "click", elementId: "e1", clickCount: 4), Parameters(action: "press_key", key: "Enter")]) }
expectError("invalid_arguments") { try validateBatchSteps([Parameters(action: "click", bundleId: "com.example", elementId: "e1"), Parameters(action: "press_key", key: "Enter")]) }
expectError("invalid_arguments") { try validateBatchSteps([Parameters(action: "set_value", elementId: "e1", value: String(repeating: "🌍", count: 8001)), Parameters(action: "type_text", elementId: "e2", text: String(repeating: "🌍", count: 8000))]) }
let tabFocus = try keyMayChangeFocus("Tab"); precondition(tabFocus)
let submitFocus = try keyMayChangeFocus("Enter"); precondition(!submitFocus)
let searchFocus = try keyMayChangeFocus("Cmd+K"); precondition(searchFocus)
Expand Down
Loading
Loading