Skip to content

feat(flow): separate what failed from what was expected and what to do - #1185

Draft
hubgan wants to merge 37 commits into
mainfrom
feat/flow-step-failure-messages
Draft

hubgan wants to merge 37 commits into
mainfrom
feat/flow-step-failure-messages

Conversation

@hubgan

@hubgan hubgan commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Why

When a flow step fails, the step report gives one text, the reason. This text has these problems:

  • The reason contains the fault and the advice together. A CI job or an agent cannot separate them.
  • A text check puts all of the element text into the reason. For a large container, the reason is as long as the text of the screen.
  • A tap on an element that is in the UI tree, but has no area on the screen, gives the same message as a tap on an element that does not exist.
  • When Argent cannot read the screen, a check fails like a usual check. A CI job cannot find these steps and run them again.

What

  • The reason of a failed step states what failed. The advice that the old reasons carried moves to hint. A few reasons still carry advice: the snapshot reason for a different app, the flow-execute prerequisite reason, and pinch/rotate on a physical iPhone.
  • A failed step can have these new fields:
    • expected: the value that the check wanted.
    • actual: the value that the check found, in full. The CLI and MCP lines show the first 300 characters of a text and then the number of characters they do not show, outside the quotes.
    • hint: a thing to try first, or a fact that helps find the cause, for example the element's own text.
  • A step has indeterminate: true when it did not do its check, because Argent could not read the UI tree:
    • an await, assert or idle step, or a when guard, whose reads failed, or came back empty and degraded;
    • a tap-family step or a snapshot with cropOn whose every read came back empty and degraded (only Vega's reader does this);
    • a tool: flow-execute step whose failed inner step had the flag.
  • A read that Argent refuses for a reason a re-run cannot change (a validation failure, for example a flow that reads an Apple system app) gets no flag and no shared hint. Its reason already says what to change.
  • The hint of a flagged step is the reader's own advice when it gave one (Vega: relaunch the app so the toolkit attaches). Otherwise it is a shared hint that puts the app first: a crash, or a screen the app emptied, reads the same as a tree source that stopped.
  • The CLI and the flow-execute MCP result show expected, actual, indeterminate and hint as lines below the step.
  • The Failed flows section of a directory run (feat(cli): show the failed flows and a re-run command at the end of a directory run #1184), and the repeat of the failed step above the result of a single run, also show these lines below the reason.
  • A tap on an element with no area on the screen now tells you that the element is in the tree. The hint gives the possible causes.
  • --json and --json-stream show the new fields. The other fields do not change. A step without the new fields is the same as before.

These steps get the new fields:

Failure New fields
tap, long-press, swipe, type, pinch, rotate or cropOn finds no element hint
assert or await with text finds a different text expected, actual, and hint when the element's own text is different
assert, await, idle or a when guard cannot read the UI tree hint, indeterminate
a tap-family step or cropOn on Vega whose every read is empty and degraded hint, indeterminate
tool: flow-execute whose composed flow failed the fields of the failed inner step, but not a snapshot's expected/actual
scroll-to does not find the target hint
snapshot has no baseline hint
snapshot has a different image size expected, actual, and hint for cropOn
snapshot difference is more than maxMismatch expected, actual

Reason texts that changed

If you search logs for an old reason text, use this list. Advice that this PR removes from a reason is now in hint.

Old reason New reason
no visible element matched selector X — if it is off-screen, add a scroll-to step before this one no element matched selector X, or N element(s) matched X but none was visible (zero-area frame)
element matched X but its text was "T" (own text "O") (wanted to equal "E") element matched X but its text did not equal "E" (also contain and match)
no baseline for "N" on this device class — expected P, nothing was compared. Run with updateBaselines … no baseline for "N" on this device class — expected P, nothing was compared
baseline is WxH but the cropOn region is WxH (K) — nothing was compared. The element's size drifted … baseline is WxH but the cropOn region is WxH (K) — nothing was compared
could not read the UI tree while waiting for the screen to settle — check the app … Underlying error: E could not read the UI tree while waiting for the screen to settle: E
the tree source never answered within the step's Nms — raise this step's timeout: … the tree source never answered within the step's Nms
the UI tree source answered and then stopped: … for the rest of the wait — check the app … the UI tree source answered and then stopped: … for the rest of the wait
the UI tree read back empty and degraded while waiting … not the app rendering nothing. <source advice> the UI tree read back empty and degraded while waiting … not the app rendering nothing
no visible element matched selector X — if it is off-screen, … (Vega cropOn, every read blind) the UI tree read back empty and degraded, so X was never looked for — this is the reader reporting it could not see the app, not the app rendering nothing
diff 0.00% > 0% diff 0.002% > 0%: more decimals when two decimals would put the value on the wrong side of maxMismatch, and the full value when only float error separates them. This also changes the reason of a passing snapshot whose rounding crossed the tolerance

How

  • In the tool-server, each directive outcome can have hint, expected and actual. The runner copies them to the step report.
  • The runner sets indeterminate and the shared hint in one location. A step with its own hint keeps it. A read refused with a validation failure gets neither.
  • After the wait for an element, the runner counts the elements that matched on the last UI tree. The count selects the reason and the hint.
  • The text check returns the values as fields. It does not put them into the reason.
  • The snapshot step returns the tolerance and the measured difference, or the two image sizes.
  • The flow recorder (flow-add-step) puts the reason, the actual text and the hint into its warning. Thus its warning keeps the data that the old reason had.
  • The CLI uses a new function, renderStepDetailLines. The live output, the full report and the directory run use it. The Failed flows section uses the same text of the lines, with the indent of that section.
  • The MCP server adds one text block below the step line.
  • The renderers change invisible characters in the values to escapes, for example \n or \u00a0: control and format characters, and each space other than U+0020. Thus a value cannot add a line to the output, and a value that differs only by such a character looks different. A quoted value, and device text that a hint quotes, use JSON escapes.

Before and after

I ran the CLI of this branch on a private headless Chrome. The "before" CLI is #1183. I removed the paths, and I made the long text shorter.

Before:

[2/9] b-tap-missing.yaml
  ✗  3 tap "Checkout" (7.5s) — no visible element matched selector text="Checkout" — if it is off-screen, add a scroll-to step before this one
[3/9] d-text-mismatch.yaml
  ✗  3 assert id=total equals "$42.00" (1.0s) — element matched id="total" but its text was "Total $41.50" (own text "Total") (wanted to equal "$42.00")
[4/9] e-text-long.yaml
  ✗  3 assert id=root-text contains "Order placed" (1.0s) — element matched id="root-text" but its text was "Line 1 lorem ipsum … (1,600 characters) … magna aliqua." (wanted to contain "Order placed")
[6/9] g-scroll-end.yaml
  ✗  3 scroll-to "Terms" in scroll container (id=list) (9.8s) — reached the end of the scroll without finding text="Terms"
[7/9] h-snapshot-missing.yaml
  ✗  3 snapshot "no-baseline-here" (0.3s) — no baseline for "no-baseline-here" on this device class — expected …/no-baseline-here__chromium-1280x713.png, nothing was compared. Run with updateBaselines (--update-baselines) to adopt the current screen, then review and commit it
[8/9] i-snapshot-diff.yaml
  ✗  4 snapshot "home" (0.4s) — diff 92.83% > 0.5% (home__chromium-1280x713.png)
       baseline: …

After:

[2/9] b-tap-missing.yaml
  ✗  3 tap "Checkout" (7.8s) — no element matched selector text="Checkout"
       hint: if it is off-screen, add a scroll-to step before this one
[3/9] d-text-mismatch.yaml
  ✗  3 assert id=total equals "$42.00" (1.0s) — element matched id="total" but its text did not equal "$42.00"
       expected: "$42.00"
       actual:   "Total $41.50"
       hint: the element's own text is "Total"; the check accepts the subtree text or the own text
[4/9] e-text-long.yaml
  ✗  3 assert id=root-text contains "Order placed" (1.0s) — element matched id="root-text" but its text did not contain "Order placed"
       expected: "Order placed"
       actual:   "Line 1 lorem ipsum … (300 characters) … Line 3 lorem ipsum dolor sit amet consecte" … (1,300 more characters)
[6/9] g-scroll-end.yaml
  ✗  3 scroll-to "Terms" in scroll container (id=list) (9.7s) — reached the end of the scroll without finding text="Terms"
       hint: the target is not in this scroll direction or not inside this scroll container; check the direction:, the within: scope and the selector
[7/9] h-snapshot-missing.yaml
  ✗  3 snapshot "no-baseline-here" (0.6s) — no baseline for "no-baseline-here" on this device class — expected …/no-baseline-here__chromium-1280x713.png, nothing was compared
       hint: run with updateBaselines (--update-baselines) to adopt the current screen, then review and commit it
[8/9] i-snapshot-diff.yaml
  ✗  4 snapshot "home" (0.5s) — diff 92.83% > 0.5% (home__chromium-1280x713.png)
       expected: ≤ 0.5%
       actual:   92.83%
       baseline: …

For this run, I stopped the Chrome during an await step. Before, the step showed only the reason. After:

Flow "l13_await"
  ✗  1 await visible "Never there" (8.0s) — could not evaluate the condition — the UI tree was unreadable for the final 6474ms of the window: [ChromiumCdp:chromium-cdp-9391] Chromium CDP discovery: GET http://127.0.0.1:9391/json/version could not connect. Is the app running with --remote-debugging-port?
       indeterminate: the check did not run
       hint: check the app first — a crash, or a screen the app emptied itself, reads the same here as a tree source that stopped answering — then check the device and the tree source; re-run before you edit the flow

In --json-stream, the same step has these fields (reason removed):

{
  "index": 0,
  "kind": "await",
  "flow": "l13_await",
  "target": "visible \"Never there\"",
  "status": "fail",
  "hint": "check the app first — a crash, or a screen the app emptied itself, reads the same here as a tree source that stopped answering — then check the device and the tree source; re-run before you edit the flow",
  "indeterminate": true,
  "durationMs": 8006
}

A flow that reads an Apple system app is refused on every run. Its step keeps the reason only, with no flag and no re-run hint (iOS 26.5 simulator):

  ✗  2 assert visible "General" (1.0s) — could not read the UI tree: com.apple.Preferences is an Apple system app (com.apple.*) - never a valid flow target: … no relaunch or retry changes this verdict. Replace the selector steps with coordinate ones …

The flow-execute MCP result, before:

[3] ✗ assert id=total equals "$42.00" (1.0s) — element matched id="total" but its text was "Total $41.50" (own text "Total") (wanted to equal "$42.00")

After:

[3] ✗ assert id=total equals "$42.00" (1.0s) — element matched id="total" but its text did not equal "$42.00"
  expected: "$42.00"
  actual:   "Total $41.50"
  hint: the element's own text is "Total"; the check accepts the subtree text or the own text

The Failed flows section at the end of a directory run. Before (main, with #1184), the reason contained the found text and the advice:

  ✗ b-text.yaml › step 2 assert id=total equals "$42.00"
    element matched id="total" but its text was "Total $41.50" (wanted to equal "$42.00")
    re-run: argent flow run flows/b-text.yaml --device chromium-cdp-9333

  ✗ c-missing.yaml › step 1 tap "Nope"
    no visible element matched selector text="Nope" — if it is off-screen, add a scroll-to step before this one
    re-run: argent flow run flows/c-missing.yaml --device chromium-cdp-9333

After:

  ✗ b-text.yaml › step 2 assert id=total equals "$42.00"
    element matched id="total" but its text did not equal "$42.00"
    expected: "$42.00"
    actual:   "Total $41.50"
    re-run: argent flow run flows/b-text.yaml --device chromium-cdp-9333

  ✗ c-missing.yaml › step 1 tap "Nope"
    no element matched selector text="Nope"
    hint: if it is off-screen, add a scroll-to step before this one
    re-run: argent flow run flows/c-missing.yaml --device chromium-cdp-9333

Tests

  • Unit tests for the tool-server cover these failures:
    • the element that has no area, with one match and with two matches;
    • the own-text hint;
    • the 300-character limit, also when the limit is in the middle of an emoji;
    • indeterminate on an await step and on a when guard;
    • a step that passes and has none of the new fields.
  • The existing tests now check the new reasons and fields.
  • The CLI and MCP tests check the lines, their sequence, their indent and control characters.
  • I ran the new tests on the old code. Each test that checks a new line or a new field failed.
  • I did these runs on a private headless Chrome with the CLI of this branch:
    • A directory run of nine flows, with and without --json. The flows fail a tap, a text check, a long text check, an await, a scroll-to and two snapshot steps, and one flow file is not valid.
    • A single run of seven of these flows, and --json for one of them.
    • An await step and a when guard. I stopped Chrome during each, and each showed the shared hint. For the await step, --json-stream showed indeterminate: true.
    • The flow-execute MCP tool, through the MCP server of this branch.
    • After the rebase on feat(cli): show the failed flows and a re-run command at the end of a directory run #1184: a directory run of four flows and a single run, with the CLI of main and with the CLI of this branch. I ran the re-run: command from the Failed flows section, and a flow with a nested flow-execute step.
  • A headless Chrome does not keep elements without area in the UI tree. Thus only the unit tests cover that message.
  • After the review fixes, I ran the branch CLI against a "before" build of the previous head, on each platform that the fixes touch:
    • iOS 26.5 simulator: an assert and a when guard on an Apple system app (no flag, no hint), and a flow that leaves the app through Safari (the final-poll note closes the reason).
    • Vega virtual device: assert and await with the toolkit not attached (the toolkit's own hint), and a cropOn miss when the device stops in the middle of the wait (the verdict of the reads that saw the screen).
    • Headless Chrome, CLI and flow-execute MCP: an own text with quotes and backslashes, a zero-width space, a nested flow-execute of a failed snapshot, a snapshot diff of 7 of 1000 pixels against maxMismatch: 0.7, and an await whose Chrome stopped (the indeterminate: line).
  • Local checks: build, the tests of all workspaces, test typecheck, lint, prettier, knip and the docs build pass. One installer test fails on my machine only, because the machine has ~/.config/opencode/opencode.jsonc. This PR does not change the installer.

Not in this PR

  • The CLI does not show (run cancelled) after Ctrl-C. Ctrl-C stops the CLI before a report comes. The tool-server stops a run only when the client closes the connection. Thus no client can show a report with aborted.

Docs

This PR updates reference/flow-yaml.mdx and features/flows.mdx. It also updates the argent-create-flow and argent-qa-flows skills: an agent records the detail lines, looks at the screen before it uses a hint, and looks at the app first when a check did not run.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

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.

Base automatically changed from feat/flow-step-timing to main September 18, 2026 11:32
@hubgan
hubgan force-pushed the feat/flow-step-failure-messages branch from 01a9d59 to 27b9a79 Compare September 18, 2026 11:59
…f dropping them

A text check that failed only on a line break or a tab printed two identical
`expected:`/`actual:` lines, in the CLI and in the MCP result alike: both
renderers replaced every control character with a space, and those lines are
the only place the found text is printed. The difference survived in `--json`
alone.

Escape the characters instead. Each value still renders on one line and no raw
escape sequence reaches the terminal.
…sted step

A `tool: flow-execute` step builds its one-line failure from the inner step's
reason. A failed check now keeps the text it found, and the advice about it,
beside that reason, so the outer step said "did not equal" with no found text
and "no element matched" with no scroll-to advice. Nothing else in a composed
run prints the inner step, and the recorder writes this step form whenever the
target is not a resolvable sibling flow.

Carry `expected`, `actual`, `hint` and `indeterminate` from that inner step
onto the outer one, where the CLI and the MCP result already print them.
… the app

An app that crashed, was terminated, or emptied its own screen after the
element was seen reads here exactly like a tree source that stopped answering:
`isBlindRead` cannot tell them apart. The shared hint still promised "this is
not a verdict on the app; re-run", so a real crash or a blank-screen
regression was labelled environment noise, and the docs repeated the claim.

Order the checks instead, and put the app first.
A failed snapshot rounded the measured diff to two decimals for display but
compared it at full precision, so the step's own expected: and actual: lines
could say the value found satisfied the value wanted: a 0.0028% diff against
`maxMismatch: 0` failed as `diff 0.00% > 0%` with `expected: ≤ 0%` and
`actual: 0.00%`. Every tolerance below 0.005% reads that way, and so does a
real mismatch that rounds down onto its tolerance.

The measured value now takes as many decimals as it needs to stay on the side
of the tolerance the comparison put it on.
…teral

For `text: { matches: … }` the `expected:` line quoted the pattern as a string
literal and doubled every backslash, so the step line one row above printed
`matches /^Taps: \d\d\d$/` while the line the feature exists for printed
`expected: "^Taps: \\d\\d\\d$"` — a valid-looking pattern that matches a
literal backslash followed by `d` when it is copied back into the flow.

Nothing on the wire said the value was a pattern. A step report now carries
`expectedKind: "pattern"` beside `expected`, and both renderers print such a
value in slash delimiters, which is the spelling `describeTextExpectation`
already uses for a pattern in the step line and the reason. `expected` keeps
the raw pattern source, so a `--json` consumer reads the same value as before.
A tree source can answer with an empty tree and its own "I could not see the
app" flags — an unattached Vega automation toolkit, or an AX service asking to
be relaunched. `assert` and `idle` already refuse to judge such a read. The
steps that resolve a frame reported `no element matched selector X` and advised
adding a `scroll-to` step, so the one step that committed to a verdict
committed to the wrong one: nothing was ever looked at, scrolling cannot help,
and editing the flow is the wrong move.

`settleTree` discarded the reader's flags when it narrowed its read to a tree,
so `waitForFrames` could not tell a blind read from an empty screen. It now
returns the whole read, the miss carries the blind-read flags, and
`selectorMiss` reports such a miss as indeterminate with the reader's own
repair. A genuinely empty screen still reports the miss and the scroll-to hint.
The failure-message skills told the agent to try the `hint:` line first,
without qualification. Most of the failure surface emits no hint, including the
commonest failures: `assert`/`await` with `exists`, `visible` or `hidden`,
`launch:`, `script:`, `run:` and every `tool:` step. The agent reading those
sentences looks for a line that is not there.
The recap that #1184 prints after a directory run, and above the verdict of a
single run, shows the reason of the first failed step. That reason used to
carry the text the step found and the advice: `its text was "Total $41.50"
(wanted to equal "$42.00")`, `… — if it is off-screen, add a scroll-to step`.
This branch moves both into the `expected:`, `actual:` and `hint:` lines, so on
top of #1184 the recap printed `its text did not equal "$42.00"` and nothing
else: the section meant to be read at the end of a long log no longer said what
the step found.

The recap now prints those lines under the reason, formatted and escaped as
under the step itself.
@hubgan
hubgan force-pushed the feat/flow-step-failure-messages branch from 27b9a79 to c146531 Compare September 21, 2026 13:44
A tree read refused with a validation failure (an Apple system app, or an
app that left the foreground) is refused again on every run. The step still
could not check the screen, so a when guard keeps erroring on it, but the
step report no longer gets indeterminate: true or the shared re-run hint.
Its reason already says what to change in the flow.
The own-text hint put the device text in raw quotes, and the renderers then
doubled its backslashes, so 'Say "hi" C:\x' printed as neither raw text nor
JSON. The tool-server now JSON-quotes the own text, the CLI and MCP escape
only control characters in a hint and a snapshot value, and the recorder
warning JSON-quotes the actual text.
The detail lines escaped only C0 controls, so a found text that differed from
the expected one by a no-break space, a U+202F, a zero-width or bidi
character, DEL or a C1 control still printed identically. The CLI and MCP now
escape all of them, in quoted values, hints, snapshot values and patterns.
…step

The outer tool step printed an inner snapshot's tolerance and diff as quoted
text (expected: "≤ 0.5%"), because the renderers leave a value unquoted only
on a snapshot step. The nested step now takes only the inner snapshot's hint.
Its reason already quotes the inner reason, which holds both values.
7 of 1000 pixels computes 0.7000000000000001%, which fails maxMismatch: 0.7.
Every fixed rounding prints 0.7, and the exponential fallback printed
7.00e-1%, so the step read 'diff 7.00e-1% > 0.7%'. The fallback now prints
the shortest exact spelling, which stays on its side of the tolerance.
A determinate assert or await whose final poll could not read the UI tree
put 'the final poll could not read the UI tree: ...' in hint:. That note
describes the read, not what to try, and on iOS it can be a whole
multi-sentence error. It closes the reason again, as before the hint
existed, and the recorder warning still ends with it.
The recorder caps its quoted verdict at 200 characters. The own-text hint
adds about 70 characters of fixed wording that the old one-sentence verdict
did not have, so a found text that used to fit was cut out of the middle.
The cap now includes that wording.
…repair

When every read came back empty with the reader's own 'I could not see the
app' flags, assert, await and when got the shared re-run hint, while tap and
idle passed on the reader's repair (on Vega: relaunch the foreground app so
the toolkit attaches). They now pass it on too, and fall back to the shared
hint only when the reader gave none.
…the screen

A cropOn (or tap-family) miss took its blind verdict from the last settled
read alone. After rounds that really read the screen and did not find the
element, one blind final read still reported 'was never looked for' with the
reader's relaunch hint. The miss is now judged on the last read that saw the
screen, and is blind only when every read in the window was.
Vega's tree keeps an off-screen node at zero area, so a cropOn that finds
only such a node reaches the zero-area hint, which said to add a scroll-to
step. Vega refuses scroll-to as a touch directive. On Vega the hint now
names the causes without that advice.
A tap-family miss or an idle wait on an empty, reader-flagged read said
'not the app rendering nothing'. When the reader gave no hint, the shared
hint under it said a screen the app emptied itself reads the same, and on a
physical iPhone, whose reader flags every childless tree, the claim was
false. The reason now states only that the screen was never read.
indeterminate: true reached only --json and --json-stream, so a reader of the
CLI output or of the flow-execute MCP result could spot a check that never
ran only from the prose of its reason. Both now print an
'indeterminate: the check did not run' line above the hint.
A swipe whose one end resolved and whose other end missed had no test, so
counting the zero-area matches of the wrong end went unnoticed. The new cases
put different match counts on the two ends, and the endpoint-miss test now
checks the exact reason and hint.
…mpt cap

Neither the 'not found after N scroll attempts' reason nor its hint was
asserted. A tree that changes on every scroll and never shows the target now
runs scroll-to to its cap.
One test paired a text mismatch with indeterminate: true, and another a diff
failure with a size-drift hint; no step reports either pair. They are split
into real shapes, and new tests cover a flow-execute nested in a
flow-execute, built from a real middle run.
The flow-visual tests call runSnapshot directly, so nothing checked that a
run's snapshot step report carries its hint, expected and actual. Runs with a
missing baseline, a diff over maxMismatch and a cropOn size change now do.
…sted

No directory-run test checked the expected, actual, indeterminate and hint
lines in both the per-flow block and the Failed flows recap, and alignment
was checked only at depth 0 with step 3. Both are now covered, including
depth 1 and 2 and steps 100 and 1000.
Where the detail block sits beside a tool step's result was untested, and no
test rendered a report the tool-server really produced. A failed nested
flow-execute step and a verbatim report from a branch run now cover both.
The skills said to try the hint first, before a screenshot and describe, so
an agent added a scroll-to step for a selector miss before it looked at the
screen. They also called every unreadable tree an environment error, left
out indeterminate steps, and said that no tool: step and no exists, visible
or hidden check has a hint. They now list the lines, say to look at the
screen before a hint, and say to look at the app first when a check did not
run.
The reference said a hint is the first thing to try and never restates the
reason, that only control characters are escaped, that a pattern can be
copied back into matches: with its slashes, and that indeterminate marks
every unreadable tree and always means run again. It now matches the code,
names the flow-execute result and the indeterminate line, and says which
steps can have the flag. The features page and the CLI help name the
detail lines in the Failed flows recap.
Several comments still described the code before this PR's changes: the
StepReport.hint doc said a hint never restates its reason, the indeterminate
doc left out the frame-resolving steps, the tier-3 note said nothing about an
empty final read, FrameMiss said matched counts every alternative, the
renderer docs left out the detail lines and said they check the status, the
pattern comments said the slashes copy back into matches:, and a physical
iPhone blind-read test still described the old settleTree.
…hint"

This reverts commit bc1e241.

The finding it fixed cannot occur. No flow tree source sends an empty,
flagged read without a hint: only Vega's reader flags an empty read, and it
always adds its relaunch hint. A physical iPhone's flow source throws on that
shape instead. So the shared hint never sits under the blind reason, and on
Vega the claim 'not the app rendering nothing' holds.
No test combined the own-text hint with a failed final poll, or capped a
long own text in the hint. The blind tap and type tests put Vega's flags on
a native-devtools read, which no source sends, and Vega refuses a tap before
it reads a tree. They now resolve a Vega cropOn frame, the one path a flow
meets that read on, and each blind test names the source that sends its
flags.
…e cuts

actual stopped at 300 characters with an ellipsis inside the quotes, so a
difference late in a large container's text was lost from the report and the
cut read as device text. The report now keeps the whole text, as the old
reason did. The CLI and MCP print its first 300 characters and then
'… (N more characters)' outside the quotes, and the own-text hint does the
same.
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