mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
56c6a9aa8e
Add a persistent isolated JavaScript worker for native app actions and batch known operations without a model round trip between each input. Preserve per-cell context, native errors, screenshot coordinates, and image types. Align macOS gesture, key, inventory, capture, scroll, and clipboard behavior; include a signed native receiver fixture and compiled sidecar regression tests. Keep the Windows pixel route and fix cancellation with session-owned mouse cleanup, lock revalidation, and portable signing-fixture tests.
210 lines
9.6 KiB
Swift
210 lines
9.6 KiB
Swift
import AppKit
|
|
#if !NATIVE_CONTRACT_STANDALONE
|
|
@testable import cc_haha_computer_use
|
|
#endif
|
|
|
|
/// The same disposable receiver is compiled standalone for official CUA and
|
|
/// hosted by the XCTest fixture child for the local native router.
|
|
@MainActor
|
|
enum NativeMethodContractFixture {
|
|
static func run(root: URL) throws {
|
|
let app = NSApplication.shared
|
|
app.setActivationPolicy(.accessory)
|
|
// Native paste is delivered through the normal Command-V responder
|
|
// chain. A standalone NSApplication has no nib-provided Edit menu.
|
|
let mainMenu = NSMenu()
|
|
let editItem = NSMenuItem(title: "Edit", action: nil, keyEquivalent: "")
|
|
let editMenu = NSMenu(title: "Edit")
|
|
editMenu.autoenablesItems = false
|
|
for (title, selector, key) in [
|
|
("Cut", #selector(NSText.cut(_:)), "x"),
|
|
("Copy", #selector(NSText.copy(_:)), "c"),
|
|
("Paste", #selector(NSText.paste(_:)), "v"),
|
|
("Select All", #selector(NSText.selectAll(_:)), "a"),
|
|
] { editMenu.addItem(withTitle: title, action: selector, keyEquivalent: key) }
|
|
editItem.submenu = editMenu
|
|
mainMenu.addItem(editItem)
|
|
app.mainMenu = mainMenu
|
|
app.finishLaunching()
|
|
let window = ContractWindow(contentRect: NSRect(x: 120, y: 160, width: 760, height: 520), styleMask: [.titled, .closable], backing: .buffered, defer: false)
|
|
window.delegate = window
|
|
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .canJoinAllApplications]
|
|
window.title = "Native method contract fixture"
|
|
let content = NSView(frame: window.contentView!.bounds)
|
|
let editor = ContractEditor(frame: NSRect(x: 20, y: 310, width: 720, height: 170))
|
|
editor.isRichText = true
|
|
editor.font = NSFont.systemFont(ofSize: 18)
|
|
editor.string = "alpha beta gamma"
|
|
editor.setAccessibilityLabel("Contract editor")
|
|
content.addSubview(editor)
|
|
let scroll = ContractScrollView(frame: NSRect(x: 20, y: 70, width: 720, height: 210))
|
|
// Native AX page increments depend on the visible clip size. Pin the
|
|
// receiver geometry instead of inheriting dynamic system scrollbar style.
|
|
scroll.scrollerStyle = .legacy
|
|
scroll.hasVerticalScroller = true
|
|
scroll.hasHorizontalScroller = true
|
|
scroll.setAccessibilityLabel("Contract scroll")
|
|
let document = NSTextView(frame: NSRect(x: 0, y: 0, width: 1400, height: 1800))
|
|
document.isEditable = false
|
|
document.font = NSFont.systemFont(ofSize: 18)
|
|
document.string = (1...70).map { "Disposable row \($0)" }.joined(separator: "\n")
|
|
document.setAccessibilityLabel("Contract rows")
|
|
scroll.documentView = document
|
|
content.addSubview(scroll)
|
|
let action = ContractSecondaryView(frame: NSRect(x: 20, y: 15, width: 400, height: 40))
|
|
action.setAccessibilityElement(true)
|
|
action.setAccessibilityRole(.button)
|
|
action.setAccessibilityLabel("Contract secondary action")
|
|
content.addSubview(action)
|
|
window.contentView = content
|
|
window.orderFrontRegardless()
|
|
window.makeMain()
|
|
window.makeFirstResponder(editor)
|
|
let ready = root.appendingPathComponent("ready")
|
|
let receipt = root.appendingPathComponent("contract.json")
|
|
var previous: Data?
|
|
let timer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { _ in
|
|
MainActor.assumeIsolated {
|
|
let selection = editor.selectedRange()
|
|
let value: [String: Any] = [
|
|
"text": editor.string,
|
|
"selectionLocation": selection.location,
|
|
"selectionLength": selection.length,
|
|
"scrollY": scroll.contentView.bounds.origin.y,
|
|
"scrollX": scroll.contentView.bounds.origin.x,
|
|
"wheel": scroll.events,
|
|
"secondaryCount": action.count,
|
|
"visible": window.isVisible,
|
|
"hidden": app.isHidden,
|
|
"windowNumber": window.windowNumber,
|
|
"windowFrame": NSStringFromRect(window.frame),
|
|
"onActiveSpace": window.isOnActiveSpace,
|
|
"miniaturized": window.isMiniaturized,
|
|
"occlusionState": window.occlusionState.rawValue,
|
|
"windowEvents": window.events,
|
|
"clipSize": NSStringFromSize(scroll.contentView.bounds.size),
|
|
"scrollerStyle": scroll.scrollerStyle.rawValue,
|
|
"editorEvents": editor.events,
|
|
]
|
|
if let data = try? JSONSerialization.data(withJSONObject: value, options: [.sortedKeys]), data != previous {
|
|
try? data.write(to: receipt, options: .atomic)
|
|
previous = data
|
|
}
|
|
if FileManager.default.fileExists(atPath: root.appendingPathComponent("stop").path) { exit(0) }
|
|
}
|
|
}
|
|
defer { timer.invalidate() }
|
|
// NSWorkspace/open -g finishes its background-launch policy after
|
|
// main() starts. Publish readiness only after that launch boundary.
|
|
Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
|
|
MainActor.assumeIsolated {
|
|
window.orderFrontRegardless()
|
|
try? Data(String(ProcessInfo.processInfo.processIdentifier).utf8).write(to: ready)
|
|
}
|
|
}
|
|
// A bounded receiver process never survives a failed parent test.
|
|
Timer.scheduledTimer(withTimeInterval: 600, repeats: false) { _ in exit(0) }
|
|
app.run()
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class ContractWindow: NSWindow, NSWindowDelegate {
|
|
var events: [[String: Any]] = []
|
|
private func record(_ kind: String, includeStack: Bool = false) {
|
|
var event: [String: Any] = [
|
|
"kind": kind, "uptime": ProcessInfo.processInfo.systemUptime,
|
|
"visible": isVisible, "miniaturized": isMiniaturized,
|
|
"onActiveSpace": isOnActiveSpace,
|
|
]
|
|
if let current = NSApplication.shared.currentEvent {
|
|
event["eventType"] = current.type.rawValue
|
|
if [.keyDown, .keyUp, .flagsChanged].contains(current.type) {
|
|
event["keyCode"] = current.keyCode
|
|
}
|
|
event["eventMarker"] = current.cgEvent?.getIntegerValueField(.eventSourceUserData)
|
|
}
|
|
if includeStack { event["stack"] = Array(Thread.callStackSymbols.prefix(12)) }
|
|
events.append(event)
|
|
if events.count > 40 { events.removeFirst(events.count - 40) }
|
|
}
|
|
override func orderOut(_ sender: Any?) {
|
|
record("orderOut", includeStack: true)
|
|
super.orderOut(sender)
|
|
}
|
|
override func close() {
|
|
record("close", includeStack: true)
|
|
super.close()
|
|
}
|
|
override func miniaturize(_ sender: Any?) {
|
|
record("miniaturize", includeStack: true)
|
|
super.miniaturize(sender)
|
|
}
|
|
func windowWillClose(_ notification: Notification) { record("willClose") }
|
|
func windowDidMiniaturize(_ notification: Notification) { record("didMiniaturize") }
|
|
func windowDidDeminiaturize(_ notification: Notification) { record("didDeminiaturize") }
|
|
func windowDidChangeOcclusionState(_ notification: Notification) { record("occlusionChanged") }
|
|
func windowDidBecomeKey(_ notification: Notification) { record("becameKey") }
|
|
func windowDidResignKey(_ notification: Notification) { record("resignedKey") }
|
|
}
|
|
|
|
@MainActor
|
|
private final class ContractScrollView: NSScrollView {
|
|
var events: [[String: Any]] = []
|
|
override func scrollWheel(with event: NSEvent) {
|
|
events.append(["x": event.scrollingDeltaX, "y": event.scrollingDeltaY, "precise": event.hasPreciseScrollingDeltas])
|
|
super.scrollWheel(with: event)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class ContractEditor: NSTextView {
|
|
var events: [[String: Any]] = []
|
|
override func paste(_ sender: Any?) {
|
|
if let name = ProcessInfo.processInfo.environment["CC_HAHA_METHOD_FIXTURE_PASTEBOARD"] {
|
|
// The real menu/responder chain consumes promised data from the
|
|
// test-owned pasteboard, without consulting the user's clipboard.
|
|
_ = readSelection(from: NSPasteboard(name: NSPasteboard.Name(name)))
|
|
} else {
|
|
super.paste(sender)
|
|
}
|
|
}
|
|
override func keyDown(with event: NSEvent) {
|
|
#if !NATIVE_CONTRACT_STANDALONE
|
|
// The test app is a receiver, never a destination for physical typing.
|
|
// Native generated keys are marked; AX writes need no keyboard event.
|
|
guard event.cgEvent?.getIntegerValueField(.eventSourceUserData) == HelperEventMarker.value else { return }
|
|
#endif
|
|
events.append(["kind": "keyDown", "keyCode": Int(event.keyCode), "flags": event.modifierFlags.rawValue])
|
|
super.keyDown(with: event)
|
|
}
|
|
override func didChangeText() {
|
|
super.didChangeText()
|
|
events.append(["kind": "changed", "text": string])
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class ContractSecondaryView: NSView {
|
|
var count = 0
|
|
override func accessibilityPerformShowMenu() -> Bool {
|
|
count += 1
|
|
needsDisplay = true
|
|
return true
|
|
}
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
NSColor.windowBackgroundColor.setFill()
|
|
dirtyRect.fill()
|
|
("Secondary actions received: \(count)" as NSString).draw(at: NSPoint(x: 5, y: 8), withAttributes: [.font: NSFont.systemFont(ofSize: 16)])
|
|
}
|
|
}
|
|
|
|
#if NATIVE_CONTRACT_STANDALONE
|
|
@main
|
|
struct NativeContractMain {
|
|
@MainActor static func main() throws {
|
|
try NativeMethodContractFixture.run(root: URL(fileURLWithPath: CommandLine.arguments[1]))
|
|
}
|
|
}
|
|
#endif
|