Skip to content

TD-5227: each agent has its own cursor on the real display - #115

Merged
captcrock merged 2 commits into
mainfrom
td/5227-agent-cursors
Nov 3, 2026
Merged

captcrock merged 2 commits into
mainfrom
td/5227-agent-cursors

Conversation

@captcrock

Copy link
Copy Markdown
Contributor

What

Every computer-use agent now has its own drawn cursor on the real display, with its own color and label. DECISIONS.md replaces the TD-3402 rule ("agent cursor on the Screen pane only") and spells out what "separate cursor" means in each mode.

Two renderers, one identity per agent:

  • tst-cu-mcp's macOS helper draws one small panel per agent (arrow plus a label pill in its color): click-through, never focused, every space, NSWindowSharingNone (left out of screenshots), one level above the ring. It goes to the agent's last move, click, or background element action, and fades 10 s after its last action. Reduced Motion: no glide, no click ring. Covers every tst-cu-mcp client: TST Desk sessions, Grok's own copy, other MCP clients.
  • agent-desktop's own multi-agent cursor for the element tools (desktop_act etc.): TST Desk runs one agent-desktop session per daemon run (session start --cursor --multi-agent --no-trace) and enables each TST Desk session with the same label and color.

Identity and color:

  • A TST Desk session is an agent labelled "TST Desk · ". The session loop sets a context variable, and the MCP driver names the acting session to the shared sidecar with the internal overlay_agent tool before each pointer action, under one lock.
  • Outside clients are labelled from MCP clientInfo.name ("Claude", "Grok", ...).
  • Colors come from a machine-wide flock slot registry, so live clients don't share one; a client that exits frees its color.

Modes: in background the pointer never moves and the drawn cursor is the only sign of where each agent works (no ring, TD-5226). In full_control the real pointer still moves for each click (there is one system pointer); the drawn cursors keep the agents apart.

Switches and Stop: the daemon mirrors "Agent cursor" and "Show on the real display" into the shared policy file (overlay.cursor, overlay.enabled); every client re-reads it live. Stop hides every drawn cursor within a quarter second and disables agent-desktop's overlay.

Host pieces (second commit): ui_act replies carry the element's frame (type-checked AXValue reads, read before the action), and captures leave agent-desktop's capturable cursor windows out via CGWindowListCreateImageFromArray (plain capture when none is on screen). Same in the Python fallbacks.

Tests (run on macOS; CI does not run tst-cu-mcp)

  • tst-cu-mcp: 659 passed; the 3 failures (DarwinBackend has no hit_test) fail the same on main. New: test_agent_cursors.py, test_overlay_rules.py cursor cases, test_ui_act_frame.py, test_darwin_capture_exclude.py. mypy: the same 4 errors as main, none added.
  • Core: test_cu_agent_cursors.py (identity, announce-under-lock, the agent-desktop session lifecycle, Stop, shared switches); ruff and mypy tstd clean.
  • Rust: cargo test --lib 148 passed; cargo clippy --all-targets -- -D warnings clean.
  • Each new test was shown to fail with its piece removed.

Live (source build, macOS, four displays)

Two MCP clients ("Claude", "Grok") pressed Calculator buttons in background mode at the same time: two cursor windows at the two buttons' centers, blue and green, pointer unchanged, real screenshots without them, both gone within a second of Stop. The packaged-app check (two TST Desk sessions, agent-desktop's cursor, Stop shortcut) runs after merge against a local build from main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HUthoCdmzg2hy89ssk2cSF

captcrock and others added 2 commits November 3, 2026 11:54
An agent doing computer use had no cursor of its own on the real display:
in full control it moved the user's pointer, and with several agents at
once nothing showed which was where. TD-3402 kept the agent cursor on the
Screen pane only; DECISIONS.md replaces that rule.

Two renderers, one identity per agent:
- tst-cu-mcp's macOS helper draws a click-through, non-shareable cursor
  panel per agent (arrow and label in its color) at its last move, click
  or acted-on element, fading 10 s after its last action. Reduced motion:
  no glide, no click ring. Colors come from a machine-wide flock slot
  registry; labels from the MCP client's name.
- The element tools run in one agent-desktop session with its own
  multi-agent cursor, each TST Desk session enabled with the same label
  and color.

Each TST Desk session is an agent ("TST Desk · <chat title>"): the
session loop sets a context variable, and the MCP driver names the acting
session with the internal overlay_agent tool before each pointer action,
under one lock. The display switches are mirrored into the shared policy
file (overlay.enabled, overlay.cursor) so every client follows them live.
Stop hides every drawn cursor and disables agent-desktop's overlay.

The helper's line protocol moves to overlay/darwin_wire.py so the helper
stays near its size limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUthoCdmzg2hy89ssk2cSF
…op's windows out

Two host pieces for per-agent cursors on the real display.

ui_act frame. A successful ui_act (press, set_value, focus, raise,
show_menu) now replies with "frame": {"x", "y", "width", "height"} in
global top-left points, the same space as CGEvent, CGWindow bounds and the
display bounds, so the caller can show the action where it happened. The
frame comes from AXPosition and AXSize and is read before the action,
because a press can close or move the element. It is best effort: when
either attribute is missing, is not an AXValue of the right type, or holds
a non-finite number, the key is left out and the action still runs.
Following TD-5225, the host checks CFGetTypeID against AXValueGetTypeID
before AXValueGetValue (cu_cf::ax_point / ax_size). The Python fallback
(darwin_ax._local_act) adds the same key through pyobjc.

Captures without agent-desktop's windows. agent-desktop draws its agent
cursor in ordinary, capturable windows, so a plain on-screen capture
showed that cursor to the model, and one agent's cursor in another
agent's screenshot. The host capture (new cu_capture module) and the
Python local capture (new darwin_capture module) now list on-screen
windows (CGWindowListCreate plus CGWindowListCreateDescriptionFromArray)
and, when any is owned by a process named exactly "agent-desktop", build
the image with CGWindowListCreateImageFromArray from the rest, front to
back. With no such window the existing CGWindowListCreateImage call runs
unchanged, and a null from-array image falls back to it. The filter is a
pure function with unit tests on both sides. The new code lives in its own
module so cu_agent.rs gets smaller; capture_png now holds the image as an
Owned reference.

Class A notes: a window whose description is missing (it closed between
the two calls) is kept; owner matching is exact and case-sensitive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HUthoCdmzg2hy89ssk2cSF
@captcrock
captcrock force-pushed the td/5227-agent-cursors branch from 8d01466 to 35b7e18 Compare November 3, 2026 16:54
@captcrock
captcrock merged commit 09fe448 into main Nov 3, 2026
4 checks passed
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.

1 participant