Skip to content

feat: describe-ui --no-raw and skill guidance to keep agent context small#63

Open
4faramita wants to merge 6 commits into
lycorp-jp:mainfrom
4faramita:feat/describe-ui-no-raw
Open

feat: describe-ui --no-raw and skill guidance to keep agent context small#63
4faramita wants to merge 6 commits into
lycorp-jp:mainfrom
4faramita:feat/describe-ui-no-raw

Conversation

@4faramita

Copy link
Copy Markdown

Summary

Two changes that reduce what an agent pays to read the screen:

  1. describe-ui --no-raw (top-level, ios, android): with --json, omit the raw accessibility tree (data.raw) from the envelope. outline / entries / lists are unchanged.
  2. A Keeping output small section in the bundled skill: prefer the text outline, pair --json with --no-raw, verify via the outline instead of screenshots, reuse the verify read as the next observe, batch known sequences.

Default behavior is untouched: bare describe-ui still prints the text outline, and --json without --no-raw still carries raw.

Measured savings

On a real app screen (40 outline entries; tokens estimated at ~4 ASCII chars/token, screenshots per Claude's pixel formula):

Change Before After Saving
--no-raw on ui --json 26.6 KB ≈ 6,600 tokens 9.7 KB ≈ 2,400 tokens -63%
Skill: text outline instead of --json ≈ 6,600 tokens/call ≈ 520 tokens/call -92%
Skill: verify via outline, not screenshot ≈ 1,500 tokens/shot ≈ 520 tokens -65%
Skill: reuse verify read as next observe 2 ui reads/step 1 read/step -45% reads

Combined on a 10-step agent task: -39% (agent already on the text loop) to -80% (agent on a --json loop) total context consumption.

Before / after

The default text outline (unchanged):

App: OrderDemo  402x874

[Top  y<120]
  @1  StaticText  "09:41"  (51,22 46x20)
  @2  RadioButton  "Orders"  (20,66 81x36)  selected
  @3  RadioButton  "Cart"  (101,66 73x36)

[Content  y=120..754]
  @8 #1  Cell  "Order #1024, 2 items"  (0,120 402x76)
  @9 #2  Cell  "Order #1023, 5 items"  (0,196 402x76)

ui --json, before (and still the default):

{
  "ok": true,
  "data": {
    "appLabel": "OrderDemo",
    "entries": [ /* full labels, frames, states */ ],
    "lists":   [ /**/ ],
    "outline": "",
    "raw":     { /* full AX tree: ~70% of the payload */ },
    "screen":  { /**/ }
  }
}

ui --json --no-raw:

{
  "ok": true,
  "data": {
    "appLabel": "OrderDemo",
    "entries": [ /* identical */ ],
    "lists":   [ /* identical */ ],
    "outline": "",
    "screen":  { /* identical */ }
  }
}

When --json is still used, and why quality is unaffected

describe-ui defaults to the text outline; agents reach for --json in two cases: coordinate math on entries[].frame / screen, and full untruncated label / value (the outline truncates labels at 60 graphemes, values at 30). Both live in entries, not in raw.

With --no-raw the envelope is byte-identical minus the raw key. raw is a debugging passthrough with no in-repo consumer (the Viewer included), no skill workflow reads it, and selector resolution (@N, --label) happens CLI-side against the AX snapshot, independent of what the agent read. Dropping it also keeps large screens from pushing the payload past agent-harness output truncation (30,000 chars in Claude Code) mid-JSON, which previously produced unparseable output and retries.

Tests

  • Tests/DescribeUINoRawTests.swift: flag parses on all three surfaces, the top-level forwarder copies it, and a result built without the tree encodes no raw key at all (not "raw": null). make test: 822 green.
  • Live spot check on a booted simulator: default text and default --json outputs byte-identical to before (modulo the status-bar clock); --no-raw envelope identical minus raw.

@4faramita
4faramita marked this pull request as ready for review July 23, 2026 08:47
@4faramita
4faramita requested a review from onevcat July 23, 2026 08:47
@4faramita 4faramita changed the title feat: describe-ui --no-raw and skill guidance to keep agent context smallFeat/describe UI no raw feat: describe-ui --no-raw and skill guidance to keep agent context small Jul 23, 2026
@4faramita
4faramita force-pushed the feat/describe-ui-no-raw branch 2 times, most recently from 43bfccb to c1c05be Compare July 23, 2026 09:04

@onevclaw onevclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I’m reviewing this as @onevcat’s assistant. The implementation and test suite look good, but two documentation issues should be corrected before merge:

  1. skills/sim-use/SKILL.md:68 presents tap --label 'X' --wait-timeout 3 as cross-platform guidance. Android's tap command does not support --wait-timeout, and the top-level Android path does not forward timing. Please scope this advice to iOS or add equivalent Android support.
  2. skills/sim-use/SKILL.md:64 says --json should be used only for untruncated text, but the same document explains that coordinate calculations need structured fields such as entries[].frame and screen. Please revise this to include structured/coordinate use cases as well.

An Android raw: nil serialization test would also be a useful follow-up, but the two documentation corrections above are the blocking items.

onevclaw - an assistant to @onevcat

@4faramita

Copy link
Copy Markdown
Author

@onevclaw Please take another claw

@4faramita
4faramita requested a review from onevclaw July 23, 2026 11:21

@onevpaw onevpaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The new --no-raw option is correctly wired through the CLI layers, but the Viewer snapshot path still invokes describe-ui --json in ViewerAPIHandlers.swift and then discards raw entirely. Since the Viewer polls this path continuously while playback is active, it continues to incur the raw-tree transfer and decoding cost this change is intended to avoid.

Please add --no-raw to that invocation and cover the spawned arguments in ViewerAPIHandlersTests.swift to prevent a regression. It would also be worth updating the comments that describe raw == nil as occurring only without --json, since --json --no-raw now has that result as well.

onevpaw - an assistant to @onevcat

@4faramita
4faramita requested a review from onevpaw July 24, 2026 04:18
4faramita and others added 6 commits July 24, 2026 13:19
data.raw is the raw accessibility tree passed through for debugging
sim-use itself; on real app screens it dominates the encoded envelope
(~70% measured on a busy screen, 26.6KB -> 9.7KB with the flag) and
larger screens can push the payload past agent tool-output truncation
limits mid-JSON. The flag is available on all three surfaces
(describe-ui, ios describe-ui, android describe-ui);
outline/entries/lists and the default --json shape are unchanged.

Signed-off-by: 4faramita <14917258+4faramita@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a "Keeping output small" section to SKILL.md: prefer the text
outline over --json, pair --json with --no-raw, verify via the outline
instead of screenshots, reuse the verify read as the next observe, and
batch known sequences. Point the cheatsheet's --json note at --no-raw.

Signed-off-by: 4faramita <14917258+4faramita@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wait-out-a-transition tip presented tap --wait-timeout as
cross-platform, but the flag is iOS-only (the Android backend has no
wait-timeout and the top-level Android path forwards no timing) --
scope the tip to iOS and point Android at sleep. The --json guidance
said "only when you need full untruncated text", contradicting the
Observe section's coordinate-math use of entries[].frame / screen --
name both use cases. Also add the Android DescribeUIResult raw-key
omission test suggested in review.

Signed-off-by: 4faramita <14917258+4faramita@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Viewer's /api/snapshot handler spawns describe-ui --json but only
forwards outline/entries/lists, discarding raw -- and it polls
continuously during playback, so it kept paying the raw-tree transfer
and decode cost the flag exists to avoid. Add --no-raw to the spawned
arguments and pin them with a fake-binary argv recording test. Also
update the two raw-nullability comments that dated from when a missing
--json was the only way to get raw == nil.

Signed-off-by: 4faramita <14917258+4faramita@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both comments spelled out the flag combination the adjacent expression
already states; keep only the cost rationale the call site can't show.

Signed-off-by: 4faramita <14917258+4faramita@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The --no-raw help text and skill claimed "3-10x" / "~70%" / "~1,500
tokens"; the measured basis was three same-session screens (raw at
2.7-3.5x the rest of the envelope) with the upper bound extrapolated,
and the screenshot token figure is specific to one client's image
tokenization. State what holds generally -- the raw tree typically
dominates the payload; a screenshot costs several times a typical
outline -- and leave exact figures out.

Signed-off-by: 4faramita <14917258+4faramita@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@4faramita
4faramita force-pushed the feat/describe-ui-no-raw branch from 385269b to 7d80124 Compare July 24, 2026 04:41
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.

3 participants