Skip to content
Merged
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
"name": "genesis-tools",
"version": "1.0.116",
"version": "1.0.117",
"description": "Plugins for GenesisTools CLI development and management",
"owner": {
"name": "genesiscz",
Expand All @@ -11,7 +11,7 @@
{
"name": "genesis-tools",
"description": "Skills and utilities for working with GenesisTools CLI toolkit. Provides guidance for discovering, executing, and troubleshooting genesis tools with integrated workflow support.",
"version": "1.0.116",
"version": "1.0.117",
"author": {
"name": "GenesisTools"
},
Expand Down
118 changes: 118 additions & 0 deletions native/ax-tool/SnapshotSupport/ForegroundGuard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Foundation

/// The quiet time, in seconds, an agent waits for before it may bring an app to the front or move the
/// pointer. Less than this since the user's last key, click, move or scroll means the user is working,
/// and an agent that took focus then typed into their window (2026-10-23: "you're taking my focus quite
/// a lot during this testing").
public let defaultForegroundIdleSeconds: TimeInterval = 5

/// `GENESIS_CONTROL_FOREGROUND_GUARD=1` turns the check on. The TypeScript runner sets it when an agent
/// drives the tool; a person typing a command just pressed Return, and must not be refused for it.
public func foregroundGuardEnabled(_ environment: [String: String]) -> Bool {
environment["GENESIS_CONTROL_FOREGROUND_GUARD"]?.trimmingCharacters(in: .whitespaces) == "1"
}

/// `GENESIS_CONTROL_FOREGROUND_IDLE=<seconds>` sets the quiet time; 0 turns the check off. A value that
/// is not a number keeps the default: a guard that a typo disables is not a guard.
public func foregroundIdleThreshold(_ environment: [String: String]) -> TimeInterval {
guard let raw = environment["GENESIS_CONTROL_FOREGROUND_IDLE"]?.trimmingCharacters(in: .whitespaces),
let value = Double(raw), value.isFinite, value >= 0 else {
return defaultForegroundIdleSeconds
}

return value
}

/// `GENESIS_CONTROL_ALLOW_FOREGROUND=1` lets a whole run take focus: a demonstration the user asked for.
public func foregroundAllowedByEnvironment(_ environment: [String: String]) -> Bool {
guard let raw = environment["GENESIS_CONTROL_ALLOW_FOREGROUND"]?.trimmingCharacters(in: .whitespaces).lowercased() else {
return false
}

return ["1", "true", "yes", "on"].contains(raw)
}

/// `--allow-foreground` lifts the guard only as a flag its command parsed, never as the value of another option:
/// `act --text --allow-foreground` types those words and keeps the guard. `arguments` is the whole command line
/// (program, command, options). `act` answers from its own parser; `launch-app` reads its two value options; every
/// other command never gets the flag from the runner and never honours it.
public func foregroundAllowedByArguments(_ arguments: [String]) -> Bool {
guard arguments.count > 2 else {
return false
}

let command = arguments[1]
let options = Array(arguments.dropFirst(2))
switch command {
case "act":
return (try? WorkflowArguments(options, command: command))?.flags.contains("--allow-foreground") == true
case "launch-app":
var index = 0
while index < options.count {
if options[index] == "--bundle-id" || options[index] == "--path" {
index += 2
continue
}

if options[index] == "--allow-foreground" {
return true
}

index += 1
}

return false
default:
return false
}
}

/// Any recent input keeps the guard closed. The window server's idle counter includes synthetic
/// posts, so subtracting our last post could hide a human event immediately before or after it.
/// Conservatively, synthetic input can extend the quiet time but can never shorten it.
public func userIsActive(idleSeconds: TimeInterval, threshold: TimeInterval) -> Bool {
threshold > 0 && idleSeconds.isFinite && idleSeconds >= 0 && idleSeconds < threshold
}

/// Admit a command's first global event independently of whether its app is already frontmost.
/// Later events in that command must not refuse on the idle counter its own first event reset.
public final class ForegroundInputGate {
private let check: () throws -> Void
private var admitted = false

public init(check: @escaping () throws -> Void) {
self.check = check
}

public func dispatch<T>(background: Bool, _ send: () throws -> T) throws -> T {
if !background && !admitted {
try check()
admitted = true
}
return try send()
}
}

/// The user is at the keyboard or mouse, so this call did not take focus. Never retried automatically.
public struct UserActiveError: Error, LocalizedError {
public let idleSeconds: TimeInterval
public let threshold: TimeInterval

public init(idleSeconds: TimeInterval, threshold: TimeInterval) {
self.idleSeconds = idleSeconds
self.threshold = threshold
}

public var category: SnapshotRefusal { .userActive }

public var errorDescription: String? {
"the user is working on this Mac (their last key, click or pointer move was "
+ String(format: "%.1f", idleSeconds) + " s ago, under the " + String(format: "%g", threshold)
+ " s quiet time), so no app was brought to the front and the pointer did not move. Act in the "
+ "background instead: click without prepare (AXPress or a window-addressed click), press_key with "
+ "activate:false, set_value or fill_form without prepare, launch_app with activate:false, open -g "
+ "for a URL. Only for a visible demonstration the user asked for: allow_foreground:true "
+ "(--allow-foreground, or GENESIS_CONTROL_ALLOW_FOREGROUND=1 for a whole run). "
+ "GENESIS_CONTROL_FOREGROUND_IDLE=0 turns this check off"
}
}
7 changes: 7 additions & 0 deletions native/ax-tool/SnapshotSupport/IsolatedComposition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import CoreImage
import CoreMedia
import Foundation

/// A single-window stream cannot filter its own target out. Refuse before starting capture.
public func validateRecordingWindow(_ id: CGWindowID, excluding excluded: Set<CGWindowID>) throws {
guard !excluded.contains(id) else {
throw WorkflowArgumentError.invalid("window \(id) is excluded by --exclude-window-ids; choose another capture target")
}
}

/// One selected window of an isolated recording. `Frame` is the last complete ScreenCaptureKit
/// surface in the recorder and a synthetic image in tests: the visibility, readiness and
/// stacking rules never look inside it.
Expand Down
2 changes: 2 additions & 0 deletions native/ax-tool/SnapshotSupport/SnapshotToken.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ public enum SnapshotRefusal: String, Codable {
case refused
/// The user put the pointer in the takeover corner. Terminal: never retried, never recovered.
case userTakeover = "user_takeover"
/// The user is at the keyboard or mouse, so an agent's call refused to take focus or the pointer.
case userActive = "user_active"
}

public enum SnapshotError: Error, LocalizedError {
Expand Down
12 changes: 11 additions & 1 deletion native/ax-tool/SnapshotSupport/UserTakeover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,20 @@ public final class SyntheticInputGate {

private let tookOver: () -> Bool
private let sleeper: (TimeInterval) -> Void
/// Runs after every event this gate sends, so the foreground guard can tell the agent's input
/// from the user's.
private let onPost: () -> Void
private var held: [(id: Int, release: () -> Void)] = []
private var nextID = 0
/// Events posted through this gate, releases included.
public private(set) var posted = 0

public init(tookOver: @escaping () -> Bool,
sleeper: @escaping (TimeInterval) -> Void = { Thread.sleep(forTimeInterval: $0) }) {
sleeper: @escaping (TimeInterval) -> Void = { Thread.sleep(forTimeInterval: $0) },
onPost: @escaping () -> Void = {}) {
self.tookOver = tookOver
self.sleeper = sleeper
self.onPost = onPost
}

/// Stop here when the user took over, after releasing anything still held (latest first).
Expand All @@ -80,6 +85,7 @@ public final class SyntheticInputGate {
while let entry = held.popLast() {
entry.release()
posted += 1
onPost()
}
throw UserTakeoverError(dispatched: posted > 0)
}
Expand All @@ -89,6 +95,7 @@ public final class SyntheticInputGate {
try check()
send()
posted += 1
onPost()
}

/// A down event. Its release runs through `release(_:)`, or through `check()` if the user takes
Expand All @@ -98,6 +105,7 @@ public final class SyntheticInputGate {
try check()
send()
posted += 1
onPost()
let id = nextID
nextID += 1
held.append((id, release))
Expand All @@ -113,13 +121,15 @@ public final class SyntheticInputGate {
let entry = held.remove(at: index)
entry.release()
posted += 1
onPost()
}

/// An event whose check already ran in the caller's own verify step, such as a drag's moves and
/// its closing release. Counted, never checked.
public func deliver(_ send: () -> Void) {
send()
posted += 1
onPost()
}

/// Sleep in slices of at most 0.1 s and check between them, so a long hold or dwell still stops
Expand Down
2 changes: 1 addition & 1 deletion native/ax-tool/SnapshotSupport/WorkflowArguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public struct WorkflowArguments {
"--revalidate-scope", "--frame", "--by-identifier", "--window-index", "--depth", "--modifiers",
"--budget-ms", "--target-ordinal", "--expect-value-sha256",
]
flagOptions = ["--background", "--allow-outside", "--double", "--refresh", "--no-cursor", "--no-image", "--prepare", "--replace", "--hold", "--no-activate"]
flagOptions = ["--background", "--allow-outside", "--double", "--refresh", "--no-cursor", "--no-image", "--prepare", "--replace", "--hold", "--no-activate", "--allow-foreground"]
default:
throw WorkflowArgumentError.invalid("unknown workflow command \(command)")
}
Expand Down
10 changes: 10 additions & 0 deletions native/ax-tool/Sources/AppLifecycle.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AppKit
import Foundation
import SnapshotSupport

func cmdInstalledApps() {
let roots = ["/Applications", "/System/Applications", "/System/Library/CoreServices",
Expand Down Expand Up @@ -60,6 +61,15 @@ func cmdLaunchApp() {
"processLaunch":launch,"frontmost":app.isActive,"alreadyRunning":existing,
"dispatchState":"dispatched"])
}
if !args.contains("--background") && !(matches.first?.isActive ?? false) {
do {
try checkForegroundAllowed()
} catch let refusal as UserActiveError {
userActiveExit(refusal)
} catch {
fputs("foreground check failed: \(error.localizedDescription)\n", stderr)
}
}
if let existing = matches.first {
if !args.contains("--background") && !existing.activate(options: [.activateIgnoringOtherApps]) {
jsonOutput(["ok":false,"error":"existing app activation was not accepted; inspect before retrying",
Expand Down
29 changes: 19 additions & 10 deletions native/ax-tool/Sources/IsolatedCapture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ struct CaptureSelectionOptions {
let transparent: Bool
let codec: String
let indicator: Bool
/// Windows the movie must never show, such as the host's own recording controls: a display capture leaves them
/// out of its content filter and an isolated capture never selects them. `NSWindow.sharingType = .none` does not
/// do it, because ScreenCaptureKit ignores it on macOS 15 and later.
let excludedWindowIDs: Set<CGWindowID>

func dimensions(points: CGSize, nativeScale: CGFloat, video: Bool) -> CGSize {
let requested = outputSize ?? CGSize(width: (points.width * (outputScale ?? nativeScale)).rounded(),
Expand All @@ -34,15 +38,19 @@ struct CaptureSelectionOptions {
}
}

/// A comma-separated list of positive CG window ids, or nil when the flag is absent.
func windowIDList(_ flag: String) -> [CGWindowID]? {
guard let raw = argValue(flag) else { return nil }
let parts = raw.split(separator: ",", omittingEmptySubsequences: false)
let ids = parts.compactMap { UInt32($0) }
guard ids.count == parts.count, ids.allSatisfy({ $0 > 0 }) else {
errorExit("\(flag) requires positive CG window IDs separated by commas")
}
return ids
}

func captureSelectionOptions(mode: String) -> CaptureSelectionOptions {
var ids: [CGWindowID] = []
if let raw = argValue("--window-ids") {
let parts = raw.split(separator: ",", omittingEmptySubsequences: false)
ids = parts.compactMap { UInt32($0) }
guard ids.count == parts.count, ids.allSatisfy({ $0 > 0 }) else {
errorExit("--window-ids requires positive CG window IDs separated by commas")
}
}
let ids = windowIDList("--window-ids") ?? []
let args = CommandLine.arguments
var apps: [String] = []
for (index, value) in args.enumerated() where value == "--include-app" {
Expand Down Expand Up @@ -94,7 +102,8 @@ func captureSelectionOptions(mode: String) -> CaptureSelectionOptions {
}
return CaptureSelectionOptions(windowIDs: Array(Set(ids)).sorted(), apps: apps, canvas: canvas,
outputSize: size, outputScale: scale, transparent: transparent,
codec: codec, indicator: !args.contains("--no-indicator"))
codec: codec, indicator: !args.contains("--no-indicator"),
excludedWindowIDs: Set(windowIDList("--exclude-window-ids") ?? []))
}

/// Same nonactivating panel contract as CursorOverlay. Core Animation supplies the pulse;
Expand Down Expand Up @@ -478,7 +487,7 @@ func captureIsolatedWindows(_ options: RecordOptions) {
let rows = captureWindowRows()
let selected = rows.compactMap { row -> SelectedWindow? in
guard let id = row[kCGWindowNumber] as? CGWindowID, let pid = row[kCGWindowOwnerPID] as? pid_t,
explicit.contains(id) || pids.contains(pid),
explicit.contains(id) || pids.contains(pid), !options.selection.excludedWindowIDs.contains(id),
let bounds = captureWindowBounds(row), bounds.width > 0, bounds.height > 0 else { return nil }
let shareable = content.windows.first { $0.windowID == id }
guard let shareable else { return nil }
Expand Down
8 changes: 7 additions & 1 deletion native/ax-tool/Sources/Record.swift
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,11 @@ func cmdCaptureScreen() {
switch options.mode {
case "window":
let target = resolveRecordWindow(options)
do {
try validateRecordingWindow(target.id, excluding: options.selection.excludedWindowIDs)
} catch {
errorExit(error.localizedDescription)
}
windowTarget = target
display = displayContaining(CGPoint(x: target.bounds.midX, y: target.bounds.midY))
case "region":
Expand Down Expand Up @@ -557,7 +562,8 @@ func cmdCaptureScreen() {
guard let scDisplay = content.displays.first(where: { $0.displayID == display.id }) else {
errorExit("display \(display.id) is not shareable")
}
filter = SCContentFilter(display: scDisplay, excludingWindows: content.windows.filter { indicator.windowIDs.contains($0.windowID) })
let excluded = indicator.windowIDs.union(options.selection.excludedWindowIDs)
Comment thread
genesiscz marked this conversation as resolved.
filter = SCContentFilter(display: scDisplay, excludingWindows: content.windows.filter { excluded.contains($0.windowID) })
pointSize = sourceRect?.size ?? display.bounds.size
}
let configuration = SCStreamConfiguration()
Expand Down
20 changes: 19 additions & 1 deletion native/ax-tool/Sources/SnapshotWorkflow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ private func workflowFailure(_ message: String, category: SnapshotRefusal = .ref
}

private func workflowFailure(_ error: Error, extras: [String: Any] = [:]) -> Never {
if let active = error as? UserActiveError {
workflowDispatchState = inputGate.posted > 0 ? "uncertain" : "not_started"
workflowFailure(active.localizedDescription, category: active.category, extras: extras)
}
if let takeover = error as? UserTakeoverError {
workflowDispatchState = takeover.dispatched ? "uncertain" : "not_started"
workflowFailure(takeover.localizedDescription, category: takeover.category, extras: extras)
Expand Down Expand Up @@ -1135,6 +1139,11 @@ func cmdAct(appName _: String) {
guard (axAttribute(element,"AXEnabled") as? NSNumber)?.boolValue != false else {
workflowFailure("selected target is disabled")
}
// Preparation raises the window and moves keyboard focus, also inside an app that is already in front. A
// prepared right-click does neither: it skips focus and raise and clicks in the background.
if !secondaryClick {
gatedOrExit { try checkForegroundAllowed() }
}
let before = tree.rows[elementIndex]
if !secondaryClick {
workflowFocus(window, pid:pid, element:window.ax)
Expand Down Expand Up @@ -1657,7 +1666,8 @@ func cmdAct(appName _: String) {
tree.rows.compactMap { $0["AXDescription"] as? String })
ActionCursor.emit("move", point: point, background: false, target: rawCoords == nil ? "ax" : "pixel")
// Checked before the warp: once it runs, the pointer is ours and no longer says
// where the user put it.
// where the user put it. A hover takes the user's pointer, so it waits for them too.
try checkForegroundAllowed()
try inputGate.post { _ = CGWarpMouseCursorPosition(point) }
if let moved = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved,
mouseCursorPosition: point, mouseButton: .left) {
Expand Down Expand Up @@ -2077,6 +2087,14 @@ func cmdAct(appName _: String) {
// why, rather than wondering whether it was silently skipped.
payload["nonActivatingPanel"] = true
}
// A window-addressed left click into an app that is not in front lands in a window that is not key.
// A SwiftUI control there takes it as the activation click and does nothing, while the click itself
// was delivered (a sheet's "Cancel" stayed open, 2026-10-23). Say so, so the caller checks the effect.
if action == "click", workflowFlag("--background"), !secondaryClick, !nonActivatingPanel, startingFrontmost != pid {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Quality | 🟡 Medium · confidence 85/100

⚠️ Potential issue

Detect inactive windows within the frontmost app too

The new effect note tests only whether another application was frontmost. When the observed target is a non-key window of the already-frontmost app, a background window-addressed click has the same activation-click limitation, but startingFrontmost equals pid and the response omits the warning. Multi-window operation is explicitly supported by the new window_id plumbing, so this misses an important supported case and callers receive plain ok:true for a potentially ineffective click. Base the note on the target window's actual key/focused-window status, not just the application's PID, and cover two windows in one app.

🧩 Analysis

Grep evidence: startingFrontmost != pid|let windowFocused = frontmostPid\(\) == pid|inactiveWindowClick

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted. The follow-up will detect inactive windows within the frontmost app too. The pull request was merged after its fix rounds, so this finding waits for a follow-up, and this thread stays open until then.

payload["inactiveWindowClick"] = true
payload["effectNote"] = "delivered to a window that is not key: a SwiftUI control may take it as the activation "
+ "click and do nothing. Check the postcondition; AXPress (a click without physical) acts without focus"
}

payload.merge(actionExtras) { _, new in new }
if workflowFlag("--refresh") {
Expand Down
Loading
Loading