Skip to content

fix(get_window_state): 30s timeout + 2000-node cap for heavy webview apps#1609

Open
hippoley wants to merge 1 commit into
trycua:mainfrom
hippoley:fix/get-window-state-timeout-and-truncation
Open

fix(get_window_state): 30s timeout + 2000-node cap for heavy webview apps#1609
hippoley wants to merge 1 commit into
trycua:mainfrom
hippoley:fix/get-window-state-timeout-and-truncation

Conversation

@hippoley
Copy link
Copy Markdown

@hippoley hippoley commented May 20, 2026

Problem

get_window_state hangs forever on Arc, Safari with many tabs, and Electron apps because AXUIElementCopyAttributeValue blocks indefinitely via XPC when the app's accessibility tree is pathologically large.

Fix

Rust (get_window_state.rs + ax/tree.rs):

  • Wrap spawn_blocking AX walk in tokio::time::timeout(30s). Returns a descriptive error suggesting capture_mode=vision or a query filter.
  • Add MAX_ELEMENTS=2000 cap to walk_element. When hit, the walk stops early and TreeWalkResult.truncated=true is set. The partial tree is returned with a warning line appended to tree_markdown.

Swift (AppState.swift):

  • Wrap renderTree in withThrowingTaskGroup with a 30s deadline task. Throws AppStateError.axWalkTimedOut(pid:) on expiry.
  • Add maxElements=2000 cap via visitedCount parameter. When hit, appends a warning line to the markdown output.
  • renderTree is now nonisolated so it can run on the detached task.

Both implementations mirror each other: 30s timeout + 2000-node cap with a user-visible warning rather than a silent hang.

Fixes #1537. Addresses #1564.

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Fixed potential hangs when processing very large accessibility trees by implementing an element traversal limit.
    • Added 30-second timeout protection for accessibility tree operations with informative error messages and suggested workaround configurations.

Review Change Stack

…apps

Fixes trycua#1537. Addresses trycua#1564.

get_window_state hangs forever on Arc, Safari with many tabs, and
Electron apps because AXUIElementCopyAttributeValue blocks indefinitely
via XPC when the app's accessibility tree is pathologically large.

Changes (Rust — get_window_state.rs + ax/tree.rs):
- Wrap spawn_blocking AX walk in tokio::time::timeout(30s). Returns a
  descriptive error suggesting capture_mode=vision or a query filter.
- Add MAX_ELEMENTS=2000 cap to walk_element. When hit, the walk stops
  early and TreeWalkResult.truncated=true is set. The partial tree is
  still returned with a warning line appended to tree_markdown.

Changes (Swift — AppState.swift):
- Wrap renderTree in withThrowingTaskGroup with a 30s deadline task.
  Throws AppStateError.axWalkTimedOut(pid:) on expiry.
- Add maxElements=2000 cap to renderTree via visitedCount parameter.
  When hit, appends a warning line to the markdown output.
- renderTree is now nonisolated so it can run on the detached task.

Both implementations mirror each other: 30s timeout + 2000-node cap
with a user-visible warning rather than a silent hang or crash.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 20, 2026

@nishantpurohit04 is attempting to deploy a commit to the Cua Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

Adds a 30-second timeout and per-walk node-count cap (2000 nodes) to AX tree traversals in both Rust and Swift layers to prevent indefinite hangs on large accessibility trees in webview-heavy applications like Arc and Safari with many tabs.

Changes

AX Tree Walk Timeout and Node-Count Bounds

Layer / File(s) Summary
Contract changes across Rust and Swift
libs/cua-driver-rs/crates/platform-macos/src/ax/tree.rs, libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift
Introduces MAX_ELEMENTS (Rust) and maxElements (Swift) cap constants, adds truncated: bool field to TreeWalkResult, and adds axWalkTimedOut(pid:) error case to AppStateError.
Rust AX walker node-count enforcement
libs/cua-driver-rs/crates/platform-macos/src/ax/tree.rs
walk_tree initializes a shared visited_count, walk_element guards against the cap and increments per node, recursive calls thread the counter forward, and truncation is signaled via TreeWalkResult.truncated with a warning appended to markdown.
Rust tool 30-second timeout wrapper
libs/cua-driver-rs/crates/platform-macos/src/tools/get_window_state.rs
get_window_state wraps the blocking AX tree walk in tokio::time::timeout with 30-second deadline, returning a specific timeout error message on deadline expiry.
Swift snapshot refactor with timeout and cap
libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift
AppStateEngine.snapshot() executes the AX tree walk in a task group with a concurrent 30-second timeout task, renderTree() tracks visitedCount and enforces maxElements via early return, and error descriptions guide users on recovery.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A walk through trees, once endless and tall,
Now bounded and swift—a timeout for all!
Two thousand nodes max, no hangs on the way,
Arc browsers rejoice! We've saved the day. 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding a 30-second timeout and 2000-node cap to fix hanging issues on heavy webview apps.
Linked Issues check ✅ Passed All primary objectives from issue #1537 are met: 30s timeout prevents indefinite hanging, 2000-node cap bounds traversal, and partial results with warnings are returned instead of blocking.
Out of Scope Changes check ✅ Passed All changes directly address the linked issues. The public API additions (TreeWalkResult.truncated field, AppStateError.axWalkTimedOut case, maxElements constant) are necessary for implementing the timeout and truncation features.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libs/cua-driver-rs/crates/platform-macos/src/ax/tree.rs`:
- Around line 127-145: Replace the current truncation check that computes
truncated = visited_count >= MAX_ELEMENTS with an explicit boolean flag that is
set only when walk_element's guard actually stops further traversal; add a
mutable truncated variable (e.g., truncated: bool) that walk_element sets to
true when it refuses to descend/queue more nodes, propagate that flag out of the
traversal and use it when constructing TreeWalkResult { tree_markdown, nodes,
truncated } and when appending the warning to tree_markdown instead of relying
on visited_count and MAX_ELEMENTS; update any references to visited_count for
display but do not use it to decide truncation.

In `@libs/cua-driver-rs/crates/platform-macos/src/tools/get_window_state.rs`:
- Around line 97-103: The error message returned by ToolResult::error in
get_window_state.rs should not recommend using "query" as a timeout workaround
because query only filters tree_markdown after the walk completes; update the
string literal passed to ToolResult::error to remove any suggestion to retry
with `query` and instead only recommend alternatives that reduce traversal work
(for example, "switch to capture_mode=vision for pixel-click workflows" or "use
capture_mode=ax with a traversal-limiting/scan-scoped filter or depth limit
applied during the AX tree walk"). Ensure you edit the exact string in the
ToolResult::error call so the misleading "query" recommendation is removed.
- Around line 90-106: The timeout currently only drops the JoinHandle but does
not stop the blocking AX walk (spawn_blocking + crate::ax::tree::walk_tree), so
repeated timeouts leak blocking threads; modify the approach so the blocking
work can be cooperatively cancelled: change walk_tree to accept a cancellation
token/flag (e.g., tokio_util::sync::CancellationToken or an Arc<AtomicBool>) and
have walk_tree periodically check it and return early, then when
tokio::time::timeout fires set/trigger that token before returning the timeout
ToolResult; update the call site (the walk_future spawn_blocking closure and its
caller) to create and pass the token, signal it on Err(_elapsed), and
await/handle the task completion or a bounded grace period to ensure the
blocking thread finishes cleanly.

In `@libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift`:
- Around line 321-326: Replace the current truncation check that uses
snapshotVisited >= AppStateEngine.maxElements with an explicit boolean flag
(e.g., didTruncate) that is set only when the traversal actually stops early
because the cap was reached (i.e., when a recursive/iterative step is skipped in
AppStateEngine's walk logic). Update the code that appends the warning to check
this didTruncate flag instead of snapshotVisited, and ensure the flag is
initialized false before traversal and flipped true at the exact point where the
traversal abandons further nodes; reference snapshotVisited,
AppStateEngine.maxElements, and the markdown append site to locate the changes.
- Around line 283-316: The snapshot task-group race is unsafe because renderTree
is synchronous/noncancellable and marked nonisolated while calling
actor-isolated helpers, and truncation uses >= causing false positives; fix by
moving the AX walk off the actor onto an unstructured background worker/thread
(e.g., create a Task.detached or DispatchThread to run renderTree) so the walk
can be abandoned independently of withThrowingTaskGroup and the timeout (replace
the current group.addTask that calls renderTree), remove the nonisolated
annotation from renderTree or make all helpers called by renderTree
(attributeString, attributeBool, topLevelChildren, children, isMenuOpen,
actionNames, cleanActionName, windows) nonisolated so actor isolation is
satisfied, and change the truncation check from if snapshotVisited >=
AppStateEngine.maxElements to if snapshotVisited > AppStateEngine.maxElements to
only truncate when the limit is exceeded; keep AppStateError.axWalkTimedOut
thrown by the timeout worker.
- Around line 560-568: renderTree is declared nonisolated but calls
actor-isolated helpers (topLevelChildren, children, isMenuOpen, attributeString,
attributeBool, actionNames); fix by crossing the actor boundary correctly:
either mark those six helper methods nonisolated if they are safe to run
off-actor, or make renderTree async (remove nonisolated) and call each helper
with await (e.g., await topLevelChildren(...)) so the calls respect actor
isolation, or alternatively move the helper implementations out of the actor
into a non-actor helper type and call those from renderTree.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c9e14423-2b0f-42fc-b589-7852179a63fd

📥 Commits

Reviewing files that changed from the base of the PR and between 66ea051 and 8d3d3e4.

📒 Files selected for processing (3)
  • libs/cua-driver-rs/crates/platform-macos/src/ax/tree.rs
  • libs/cua-driver-rs/crates/platform-macos/src/tools/get_window_state.rs
  • libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift

Comment on lines +127 to 145
let truncated = visited_count >= MAX_ELEMENTS;
let raw_markdown = render_lines(&lines);
let tree_markdown = if let Some(q) = query {
let mut tree_markdown = if let Some(q) = query {
filter_tree(&raw_markdown, q)
} else {
raw_markdown
};

TreeWalkResult { tree_markdown, nodes }
if truncated {
tree_markdown.push_str(&format!(
"\n⚠️ AX tree truncated at {MAX_ELEMENTS} nodes \
(app has a very large accessibility tree — Arc, Electron, or similar). \
Element indices above are still valid. Use pixel clicks for elements \
not visible in this partial tree."
));
}

TreeWalkResult { tree_markdown, nodes, truncated }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use an explicit truncation flag instead of visited_count >= MAX_ELEMENTS.

A walk that naturally ends on the 2,000th visited node will still be marked truncated and get the warning even though no recursion was cut short. Track a separate truncated flag when the guard in walk_element actually rejects more work, and use that here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/crates/platform-macos/src/ax/tree.rs` around lines 127 -
145, Replace the current truncation check that computes truncated =
visited_count >= MAX_ELEMENTS with an explicit boolean flag that is set only
when walk_element's guard actually stops further traversal; add a mutable
truncated variable (e.g., truncated: bool) that walk_element sets to true when
it refuses to descend/queue more nodes, propagate that flag out of the traversal
and use it when constructing TreeWalkResult { tree_markdown, nodes, truncated }
and when appending the warning to tree_markdown instead of relying on
visited_count and MAX_ELEMENTS; update any references to visited_count for
display but do not use it to decide truncation.

Comment on lines +90 to 106
let walk_future = tokio::task::spawn_blocking(move || {
crate::ax::tree::walk_tree(pid, Some(window_id), q.as_deref())
}).await;
match result {
Ok(r) => Some(r),
Err(e) => return ToolResult::error(format!("AX tree walk failed: {e}")),
});
match tokio::time::timeout(std::time::Duration::from_secs(30), walk_future).await {
Ok(Ok(r)) => Some(r),
Ok(Err(e)) => return ToolResult::error(format!("AX tree walk failed: {e}")),
Err(_elapsed) => {
return ToolResult::error(format!(
"AX tree walk for pid={pid} timed out after 30 s. \
The app (likely Arc, Electron, or Safari with many tabs) has a \
pathologically large accessibility tree. \
Workarounds: switch to capture_mode=vision for pixel-click \
workflows, or use capture_mode=ax with a query filter to \
limit the walk scope."
));
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

In Tokio, if tokio::time::timeoutexpires while awaiting aspawn_blocking JoinHandle, does the underlying blocking task stop, or does it continue running after the handle is dropped?

💡 Result:

If tokio::time::timeout expires while you’re awaiting a JoinHandle returned by tokio::task::spawn_blocking, the underlying blocking closure does not get stopped; it continues running in the background after the JoinHandle is dropped. Tokio’s docs state that spawn_blocking closures “cannot be aborted because they are not async” and that even calling abort() on a spawn_blocking task “will not have any effect” (once it has started). It also explains that when a Tokio runtime is shut down it “will attempt to abort all tasks including spawn_blocking tasks,” but blocking tasks “cannot be aborted once they start running,” and shutdown may still wait for them to finish. [1][2] Separately, Tokio’s JoinHandle behavior is that dropping the handle “detaches the associated task,” and “the task continues running in the background and its return value is lost.” [3][4] So, in the specific scenario you described: - timeout(...).await timing out typically means the await is dropped (i.e., the JoinHandle variable goes out of scope / is dropped). - Dropping the JoinHandle detaches the task, so the blocking closure keeps running. - Additionally, even if you were to try to abort the handle, spawn_blocking tasks can’t be aborted once they’ve started. [1][3]

Citations:


Timeout does not stop the blocking task—the AX walk continues running in the background.

When tokio::time::timeout expires on the spawn_blocking JoinHandle, the underlying blocking closure is not aborted; it continues consuming a blocking thread in the pool. Repeated timeouts can accumulate idle threads and starve the blocking pool for future work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/crates/platform-macos/src/tools/get_window_state.rs`
around lines 90 - 106, The timeout currently only drops the JoinHandle but does
not stop the blocking AX walk (spawn_blocking + crate::ax::tree::walk_tree), so
repeated timeouts leak blocking threads; modify the approach so the blocking
work can be cooperatively cancelled: change walk_tree to accept a cancellation
token/flag (e.g., tokio_util::sync::CancellationToken or an Arc<AtomicBool>) and
have walk_tree periodically check it and return early, then when
tokio::time::timeout fires set/trigger that token before returning the timeout
ToolResult; update the call site (the walk_future spawn_blocking closure and its
caller) to create and pass the token, signal it on Err(_elapsed), and
await/handle the task completion or a bounded grace period to ensure the
blocking thread finishes cleanly.

Comment on lines +97 to +103
return ToolResult::error(format!(
"AX tree walk for pid={pid} timed out after 30 s. \
The app (likely Arc, Electron, or Safari with many tabs) has a \
pathologically large accessibility tree. \
Workarounds: switch to capture_mode=vision for pixel-click \
workflows, or use capture_mode=ax with a query filter to \
limit the walk scope."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don't recommend query as a timeout workaround here.

This file's own tool description says query only filters tree_markdown after the walk completes. Users who retry with query will hit the same timeout because it does not shrink the traversal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver-rs/crates/platform-macos/src/tools/get_window_state.rs`
around lines 97 - 103, The error message returned by ToolResult::error in
get_window_state.rs should not recommend using "query" as a timeout workaround
because query only filters tree_markdown after the walk completes; update the
string literal passed to ToolResult::error to remove any suggestion to retry
with `query` and instead only recommend alternatives that reduce traversal work
(for example, "switch to capture_mode=vision for pixel-click workflows" or "use
capture_mode=ax with a traversal-limiting/scan-scoped filter or depth limit
applied during the AX tree walk"). Ensure you edit the exact string in the
ToolResult::error call so the misleading "query" recommendation is removed.

Comment on lines +283 to +316
let (snapshotMarkdown, snapshotElements, snapshotVisited) = try await withThrowingTaskGroup(
of: (String, [Int: AXUIElement], Int).self
) { group in
group.addTask {
let root = AXUIElementCreateApplication(pid)
var els: [Int: AXUIElement] = [:]
var idx = 0
var md = ""
var visited = 0
let layer0WindowIds: Set<CGWindowID> = Set(
WindowEnumerator.allWindows()
.filter { $0.layer == 0 }
.map { CGWindowID($0.id) }
)
self.renderTree(
root,
depth: 0,
targetWindowId: windowId,
layer0WindowIds: layer0WindowIds,
elements: &els,
nextIndex: &idx,
output: &md,
visitedCount: &visited
)
return (md, els, visited)
}
group.addTask {
try await Task.sleep(nanoseconds: 30_000_000_000)
throw AppStateError.axWalkTimedOut(pid: pid)
}
let result = try await group.next()!
group.cancelAll()
return result
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Does Swift withThrowingTaskGroup wait for canceled child tasks to finish before the group scope exits, and can it enforce a timeout around synchronous work that never checks cancellation?

💡 Result:

Yes, Swift's withThrowingTaskGroup always waits for all of its child tasks to complete, including any that were canceled, before the group's scope exits [1][2]. This behavior is a fundamental guarantee of Swift's structured concurrency, ensuring that child tasks do not become "orphaned" or continue running unexpectedly after the group has finished [2][3]. Regarding your second question, withThrowingTaskGroup cannot enforce a timeout on synchronous work that does not check for cancellation [4]. Swift's concurrency model relies on cooperative cancellation [5][4]. When you cancel a task group (e.g., via cancelAll), it merely sets a cancellation flag on the child tasks [2][5]. It is the responsibility of the code within those tasks to periodically check this flag (using Task.isCancelled or try Task.checkCancellation) and exit voluntarily [2][5][6]. If a child task is performing long-running synchronous work or blocking operations that do not include such checks, it will ignore the cancellation signal and continue executing until the work is finished [4]. Consequently, even if you implement a timeout mechanism—typically by racing your work against a sleeping task in a group—the group will remain suspended, waiting for that non-cooperative task to complete before it can finally return [1][4]. To ensure a timeout works as expected, you must manually ensure that the long-running operation is designed to respond to cancellation [4][6].

Citations:


🏁 Script executed:

#!/bin/bash
# Check the renderTree method signature and verify nonisolated marking
rg -A 20 "func renderTree" libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift

Repository: trycua/cua

Length of output: 952


🏁 Script executed:

#!/bin/bash
# Check for truncation warning logic
rg "truncation|maxElements|AppStateEngine" libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift

Repository: trycua/cua

Length of output: 409


🏁 Script executed:

#!/bin/bash
# Verify the file is part of an actor and check class/actor declaration
head -100 libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift | grep -E "^(class|actor|struct)"

Repository: trycua/cua

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Search for the declaration of attributeString and attributeBool
rg -B 2 "func attributeString|func attributeBool" libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift | head -40

Repository: trycua/cua

Length of output: 249


🏁 Script executed:

#!/bin/bash
# Also check for topLevelChildren, children, isMenuOpen, actionNames, cleanActionName, windows
rg "func topLevelChildren|func children|func isMenuOpen|func actionNames|func cleanActionName|func windows" libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift | head -20

Repository: trycua/cua

Length of output: 458


This task-group race does not guarantee a 30-second return.

If the sleeper throws first, withThrowingTaskGroup still has to unwind the render child before the scope can exit. renderTree is synchronous and the AX calls inside it do not observe cancellation, so a blocked AX walk can still keep snapshot() hung past the deadline. This misses the bounded-time objective unless the walk is moved behind an unstructured worker/thread boundary that can be abandoned independently.

Additionally, renderTree is marked private nonisolated but calls actor-isolated instance methods (attributeString, attributeBool, topLevelChildren, children, isMenuOpen, actionNames, cleanActionName, windows), which violates Swift actor isolation rules and will cause a compile error. The method must either be non-nonisolated or all helper calls must be nonisolated.

The truncation logic checks if snapshotVisited >= AppStateEngine.maxElements, replicating the exact-cap false positive from the Rust implementation—truncation occurs at exactly 2_000 nodes rather than exceeding the limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift` around lines
283 - 316, The snapshot task-group race is unsafe because renderTree is
synchronous/noncancellable and marked nonisolated while calling actor-isolated
helpers, and truncation uses >= causing false positives; fix by moving the AX
walk off the actor onto an unstructured background worker/thread (e.g., create a
Task.detached or DispatchThread to run renderTree) so the walk can be abandoned
independently of withThrowingTaskGroup and the timeout (replace the current
group.addTask that calls renderTree), remove the nonisolated annotation from
renderTree or make all helpers called by renderTree (attributeString,
attributeBool, topLevelChildren, children, isMenuOpen, actionNames,
cleanActionName, windows) nonisolated so actor isolation is satisfied, and
change the truncation check from if snapshotVisited >=
AppStateEngine.maxElements to if snapshotVisited > AppStateEngine.maxElements to
only truncate when the limit is exceeded; keep AppStateError.axWalkTimedOut
thrown by the timeout worker.

Comment on lines +321 to +326
if snapshotVisited >= AppStateEngine.maxElements {
markdown += "\n⚠️ AX tree truncated at \(AppStateEngine.maxElements) nodes"
+ " (app has a very large accessibility tree — Arc, Electron, or similar)."
+ " Element indices above are still valid. Use pixel clicks for elements"
+ " not visible in this partial tree."
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use an explicit truncation flag instead of snapshotVisited >= maxElements.

A walk that finishes naturally on exactly element 2,000 will still append the truncation warning. Set the warning off a flag that is flipped only when a recursive step is skipped because the cap was already reached.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift` around lines
321 - 326, Replace the current truncation check that uses snapshotVisited >=
AppStateEngine.maxElements with an explicit boolean flag (e.g., didTruncate)
that is set only when the traversal actually stops early because the cap was
reached (i.e., when a recursive/iterative step is skipped in AppStateEngine's
walk logic). Update the code that appends the warning to check this didTruncate
flag instead of snapshotVisited, and ensure the flag is initialized false before
traversal and flipped true at the exact point where the traversal abandons
further nodes; reference snapshotVisited, AppStateEngine.maxElements, and the
markdown append site to locate the changes.

Comment on lines +560 to +568
private nonisolated func renderTree(
_ element: AXUIElement,
depth: Int,
targetWindowId: UInt32?,
layer0WindowIds: Set<CGWindowID> = [],
elements: inout [Int: AXUIElement],
nextIndex: inout Int,
output: inout String
output: inout String,
visitedCount: inout Int
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In Swift actors, can a nonisolatedinstance method synchronously call actor-isolated instance methods, or must those helpers also be markednonisolated/static?

💡 Result:

In Swift, a nonisolated instance method cannot synchronously call an actor-isolated instance method [1][2]. Actor-isolated methods are protected by the actor’s serial executor, ensuring that they can only be accessed sequentially from within the actor or asynchronously via await from outside [3][4][2]. Because a nonisolated method is explicitly opted out of this isolation—meaning it does not have safe access to the actor's mutable state—calling an isolated method synchronously from a nonisolated context would violate the actor's thread-safety guarantees [1][3][2]. To call an actor-isolated method from a nonisolated context, you must use the await keyword [3][4]. This introduces a potential suspension point, allowing the system to safely hop onto the actor's executor to perform the operation [1][2]. If you find yourself needing to perform such calls frequently, these alternatives are commonly used: 1. Asynchronous Calls: Use await to bridge the isolation boundary [3][2]. 2. Isolated Parameters: If you are in a context where you have access to the actor, you can use an isolated parameter on a function to run synchronously on that actor's executor without needing await [5][4][6]. 3. Architecture Refactoring: If the "helper" logic does not actually require access to the actor’s mutable state, you can mark the helper method itself as nonisolated [3][4]. If it does require access, it must remain isolated and be called as an asynchronous operation [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
# Find the AppState.swift file and examine the renderTree method
find . -name "AppState.swift" -path "*CuaDriverCore*" | head -5

Repository: trycua/cua

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Check the file size and read the relevant section
wc -l "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift"

Repository: trycua/cua

Length of output: 120


🏁 Script executed:

#!/bin/bash
# Read the renderTree method (lines 560-620 to see the full implementation)
sed -n '560,620p' "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | cat -n

Repository: trycua/cua

Length of output: 3350


🏁 Script executed:

#!/bin/bash
# Search for the helper methods to check their isolation status
rg -n "(topLevelChildren|children|isMenuOpen|attributeString|attributeBool|actionNames)\s*\(" "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" -A 3 | head -60

Repository: trycua/cua

Length of output: 3049


🏁 Script executed:

#!/bin/bash
# Find the class declaration for AppState - search backwards from line 560
sed -n '1,560p' "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | grep -E "^(actor|class|struct)" | tail -3

Repository: trycua/cua

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Get more context around the class declaration
head -50 "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | cat -n

Repository: trycua/cua

Length of output: 2955


🏁 Script executed:

#!/bin/bash
# Search for the actor declaration containing these methods
rg -n "^(actor|class|struct)\s+AppState" "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | head -5

Repository: trycua/cua

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Search more broadly for actor keyword in this file
rg -n "^actor\s" "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift"

Repository: trycua/cua

Length of output: 36


🏁 Script executed:

#!/bin/bash
# Search for AppState declaration with different patterns
rg -n "class AppState|actor AppState|struct AppState" "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift"

Repository: trycua/cua

Length of output: 143


🏁 Script executed:

#!/bin/bash
# Look for the main type definition in the file
sed -n '50,150p' "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | cat -n

Repository: trycua/cua

Length of output: 6038


🏁 Script executed:

#!/bin/bash
# Verify the helper methods are NOT marked nonisolated
sed -n '680,790p' "libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift" | grep -E "private (nonisolated )?func (topLevelChildren|children|isMenuOpen|attributeString|attributeBool|actionNames)"

Repository: trycua/cua

Length of output: 507


Mark helper methods nonisolated or refactor to bridge the actor isolation boundary.

The renderTree method is marked nonisolated but calls actor-isolated instance methods (topLevelChildren, children, isMenuOpen, attributeString, attributeBool, actionNames) synchronously. Swift actors prohibit this—a nonisolated method cannot directly call actor-isolated methods without await. Either mark all six helper methods as nonisolated, make renderTree async and use await for these calls, or move the helpers outside the actor.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/cua-driver/Sources/CuaDriverCore/AppState/AppState.swift` around lines
560 - 568, renderTree is declared nonisolated but calls actor-isolated helpers
(topLevelChildren, children, isMenuOpen, attributeString, attributeBool,
actionNames); fix by crossing the actor boundary correctly: either mark those
six helper methods nonisolated if they are safe to run off-actor, or make
renderTree async (remove nonisolated) and call each helper with await (e.g.,
await topLevelChildren(...)) so the calls respect actor isolation, or
alternatively move the helper implementations out of the actor into a non-actor
helper type and call those from renderTree.

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.

get_window_state hangs forever on heavy webview apps (Arc, Safari w/ many tabs) on macOS 26

1 participant