mobster-cli 0.4.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1009) hide show
  1. mobile_agent/__init__.py +4 -0
  2. mobile_agent/__main__.py +598 -0
  3. mobile_agent/accounts/__init__.py +1 -0
  4. mobile_agent/accounts/context.py +80 -0
  5. mobile_agent/accounts/mcp.py +55 -0
  6. mobile_agent/accounts/pacing.py +318 -0
  7. mobile_agent/accounts/register.py +78 -0
  8. mobile_agent/accounts/routes.py +212 -0
  9. mobile_agent/accounts/routing.py +69 -0
  10. mobile_agent/accounts/screen.py +273 -0
  11. mobile_agent/accounts/stats.py +54 -0
  12. mobile_agent/accounts/store.py +394 -0
  13. mobile_agent/agent_hooks.py +123 -0
  14. mobile_agent/ai_plans/__init__.py +7 -0
  15. mobile_agent/ai_plans/agents.py +96 -0
  16. mobile_agent/ai_plans/claude_code.py +208 -0
  17. mobile_agent/ai_plans/cli.py +400 -0
  18. mobile_agent/ai_plans/codex.py +258 -0
  19. mobile_agent/ai_plans/compat_chat.py +307 -0
  20. mobile_agent/ai_plans/device_login.py +164 -0
  21. mobile_agent/ai_plans/draft_client.py +68 -0
  22. mobile_agent/ai_plans/gemini.py +137 -0
  23. mobile_agent/ai_plans/instructions.py +65 -0
  24. mobile_agent/ai_plans/mcp_http.py +160 -0
  25. mobile_agent/ai_plans/phone_tools.py +892 -0
  26. mobile_agent/ai_plans/providers.py +235 -0
  27. mobile_agent/ai_plans/routes.py +160 -0
  28. mobile_agent/ai_plans/runner.py +400 -0
  29. mobile_agent/ai_plans/vault.py +114 -0
  30. mobile_agent/alerts.py +337 -0
  31. mobile_agent/android/__init__.py +18 -0
  32. mobile_agent/android/adb.py +268 -0
  33. mobile_agent/android/conditions.py +128 -0
  34. mobile_agent/android/doctor.py +54 -0
  35. mobile_agent/android/driver.py +137 -0
  36. mobile_agent/android/emulator.py +216 -0
  37. mobile_agent/android/logs.py +85 -0
  38. mobile_agent/android/manager.py +225 -0
  39. mobile_agent/android/records.py +31 -0
  40. mobile_agent/android/tree.py +96 -0
  41. mobile_agent/answer_shapes.py +230 -0
  42. mobile_agent/api_devices.py +333 -0
  43. mobile_agent/api_errors.py +17 -0
  44. mobile_agent/api_get.py +249 -0
  45. mobile_agent/api_http.py +467 -0
  46. mobile_agent/api_post.py +453 -0
  47. mobile_agent/api_routes.py +173 -0
  48. mobile_agent/app_api/__init__.py +4 -0
  49. mobile_agent/app_api/cli.py +151 -0
  50. mobile_agent/app_api/endpoints.py +193 -0
  51. mobile_agent/app_api/mcp.py +54 -0
  52. mobile_agent/app_api/openapi.py +146 -0
  53. mobile_agent/app_api/reading.py +172 -0
  54. mobile_agent/app_api/register.py +10 -0
  55. mobile_agent/app_api/screen.py +216 -0
  56. mobile_agent/app_api/service.py +382 -0
  57. mobile_agent/app_api/sessions.py +404 -0
  58. mobile_agent/app_api/tokens.py +167 -0
  59. mobile_agent/app_choice.py +131 -0
  60. mobile_agent/app_icons.py +183 -0
  61. mobile_agent/app_killer.py +69 -0
  62. mobile_agent/artifacts.py +606 -0
  63. mobile_agent/attach/__init__.py +13 -0
  64. mobile_agent/attach/cli.py +46 -0
  65. mobile_agent/attach/flutter.py +28 -0
  66. mobile_agent/attach/score.py +119 -0
  67. mobile_agent/attach/web.py +13 -0
  68. mobile_agent/attachments/__init__.py +1 -0
  69. mobile_agent/attachments/agent.py +418 -0
  70. mobile_agent/attachments/extract.py +217 -0
  71. mobile_agent/attachments/mcp.py +104 -0
  72. mobile_agent/attachments/register.py +73 -0
  73. mobile_agent/attachments/routes.py +326 -0
  74. mobile_agent/attachments/service.py +170 -0
  75. mobile_agent/attachments/sniff.py +152 -0
  76. mobile_agent/attachments/store.py +441 -0
  77. mobile_agent/audit_verify.py +436 -0
  78. mobile_agent/bench/README.md +410 -0
  79. mobile_agent/bench/__init__.py +0 -0
  80. mobile_agent/bench/__main__.py +394 -0
  81. mobile_agent/bench/ab.py +168 -0
  82. mobile_agent/bench/actuation.py +368 -0
  83. mobile_agent/bench/agents/__init__.py +9 -0
  84. mobile_agent/bench/agents/base.py +105 -0
  85. mobile_agent/bench/agents/plan_clients.py +245 -0
  86. mobile_agent/bench/agents/smart.py +533 -0
  87. mobile_agent/bench/checks.py +358 -0
  88. mobile_agent/bench/decide_gate.py +214 -0
  89. mobile_agent/bench/diagnose.py +181 -0
  90. mobile_agent/bench/evals/__init__.py +14 -0
  91. mobile_agent/bench/evals/build.py +565 -0
  92. mobile_agent/bench/evals/checkpoint.py +214 -0
  93. mobile_agent/bench/evals/irt.py +219 -0
  94. mobile_agent/bench/evals/l0.py +647 -0
  95. mobile_agent/bench/evals/l1.py +866 -0
  96. mobile_agent/bench/evals/models.py +124 -0
  97. mobile_agent/bench/evals/pairwise.py +189 -0
  98. mobile_agent/bench/evals/proxy.py +90 -0
  99. mobile_agent/bench/evals/sets.py +149 -0
  100. mobile_agent/bench/evals/traces.py +446 -0
  101. mobile_agent/bench/fixtures.py +176 -0
  102. mobile_agent/bench/guard_replay.py +129 -0
  103. mobile_agent/bench/h2-manifest.json +20 -0
  104. mobile_agent/bench/h3-manifest.json +20 -0
  105. mobile_agent/bench/heldout-manifest.json +32 -0
  106. mobile_agent/bench/heldout.py +375 -0
  107. mobile_agent/bench/iosworld.py +901 -0
  108. mobile_agent/bench/iosworld_stats.py +510 -0
  109. mobile_agent/bench/llm_units.py +116 -0
  110. mobile_agent/bench/manifest.json +93 -0
  111. mobile_agent/bench/metrics.py +66 -0
  112. mobile_agent/bench/phone.py +44 -0
  113. mobile_agent/bench/replay.py +172 -0
  114. mobile_agent/bench/report.py +207 -0
  115. mobile_agent/bench/runner.py +456 -0
  116. mobile_agent/bench/safety.py +310 -0
  117. mobile_agent/bench/stream_timing.py +213 -0
  118. mobile_agent/bench/suite.py +444 -0
  119. mobile_agent/bench/sweep.py +176 -0
  120. mobile_agent/bench/tape.py +798 -0
  121. mobile_agent/bench/tape_replay.py +88 -0
  122. mobile_agent/bench/trace_evals.py +128 -0
  123. mobile_agent/bench/trace_view.py +81 -0
  124. mobile_agent/bench/truth/iphone15pro.json +138 -0
  125. mobile_agent/bench/truth.py +340 -0
  126. mobile_agent/bench/tuning.py +543 -0
  127. mobile_agent/bench/tuning_analysis.py +656 -0
  128. mobile_agent/bench/vague-heldout-manifest.json +12 -0
  129. mobile_agent/bench/vague_rubric.py +135 -0
  130. mobile_agent/bench/vague_run.py +243 -0
  131. mobile_agent/brand.py +454 -0
  132. mobile_agent/brand_fonts.py +756 -0
  133. mobile_agent/build_identity.py +21 -0
  134. mobile_agent/cable_bt.py +623 -0
  135. mobile_agent/cable_bt_runtime.py +128 -0
  136. mobile_agent/catalog.py +69 -0
  137. mobile_agent/chart_svg.py +415 -0
  138. mobile_agent/chat_here.py +189 -0
  139. mobile_agent/cli_constants.py +5 -0
  140. mobile_agent/cli_parser.py +611 -0
  141. mobile_agent/cli_run.py +115 -0
  142. mobile_agent/compiled_read/__init__.py +21 -0
  143. mobile_agent/compiled_read/delta.py +89 -0
  144. mobile_agent/compiled_read/extractors.py +202 -0
  145. mobile_agent/compiled_read/routes.py +56 -0
  146. mobile_agent/completion.py +267 -0
  147. mobile_agent/compose.py +48 -0
  148. mobile_agent/config.py +114 -0
  149. mobile_agent/connectors/__init__.py +1 -0
  150. mobile_agent/connectors/api_session.py +76 -0
  151. mobile_agent/connectors/callback_page.py +146 -0
  152. mobile_agent/connectors/client.py +582 -0
  153. mobile_agent/connectors/github_app.py +55 -0
  154. mobile_agent/connectors/graph_api.py +396 -0
  155. mobile_agent/connectors/imports.py +170 -0
  156. mobile_agent/connectors/issue.py +66 -0
  157. mobile_agent/connectors/keystore.py +85 -0
  158. mobile_agent/connectors/marks.py +222 -0
  159. mobile_agent/connectors/oauth.py +476 -0
  160. mobile_agent/connectors/picker.py +228 -0
  161. mobile_agent/connectors/presets.py +591 -0
  162. mobile_agent/connectors/register.py +48 -0
  163. mobile_agent/connectors/routes.py +153 -0
  164. mobile_agent/connectors/service.py +966 -0
  165. mobile_agent/connectors/shared.py +136 -0
  166. mobile_agent/connectors/skill.py +446 -0
  167. mobile_agent/connectors/slack_api.py +269 -0
  168. mobile_agent/connectors/steps.py +92 -0
  169. mobile_agent/connectors/store.py +144 -0
  170. mobile_agent/connectors/vault.py +72 -0
  171. mobile_agent/console.py +178 -0
  172. mobile_agent/content_queue/__init__.py +1 -0
  173. mobile_agent/content_queue/client.py +263 -0
  174. mobile_agent/content_queue/importing.py +153 -0
  175. mobile_agent/content_queue/media_check.py +224 -0
  176. mobile_agent/content_queue/prompt.py +64 -0
  177. mobile_agent/content_queue/register.py +91 -0
  178. mobile_agent/content_queue/routes.py +370 -0
  179. mobile_agent/content_queue/runner.py +475 -0
  180. mobile_agent/content_queue/store.py +521 -0
  181. mobile_agent/contract.py +996 -0
  182. mobile_agent/contract_asks.py +248 -0
  183. mobile_agent/contract_prompt.py +45 -0
  184. mobile_agent/contract_screen.py +981 -0
  185. mobile_agent/contract_view.py +84 -0
  186. mobile_agent/corpus/__init__.py +26 -0
  187. mobile_agent/corpus/cli.py +63 -0
  188. mobile_agent/corpus/consent.py +55 -0
  189. mobile_agent/corpus/listener.py +76 -0
  190. mobile_agent/corpus/register.py +9 -0
  191. mobile_agent/corpus/skeleton.py +72 -0
  192. mobile_agent/corpus/store.py +143 -0
  193. mobile_agent/costs.py +161 -0
  194. mobile_agent/decider_scope.py +40 -0
  195. mobile_agent/decision_provider.py +189 -0
  196. mobile_agent/deeplinks.py +352 -0
  197. mobile_agent/device_info.py +116 -0
  198. mobile_agent/device_manager.py +978 -0
  199. mobile_agent/device_status.py +195 -0
  200. mobile_agent/device_targets.py +217 -0
  201. mobile_agent/devices.py +676 -0
  202. mobile_agent/devkit/__init__.py +64 -0
  203. mobile_agent/devkit/account_cli.py +90 -0
  204. mobile_agent/devkit/asc.py +562 -0
  205. mobile_agent/devkit/asc_cli.py +214 -0
  206. mobile_agent/devkit/ci.py +737 -0
  207. mobile_agent/devkit/ci_cli.py +196 -0
  208. mobile_agent/devkit/ci_hosts.py +194 -0
  209. mobile_agent/devkit/es256.py +197 -0
  210. mobile_agent/devkit/expo.py +440 -0
  211. mobile_agent/devkit/expo_cli.py +405 -0
  212. mobile_agent/devkit/expo_dev.py +203 -0
  213. mobile_agent/devkit/expo_routes.py +90 -0
  214. mobile_agent/devkit/fixtures_cli.py +133 -0
  215. mobile_agent/devkit/fixtures_routes.py +82 -0
  216. mobile_agent/devkit/links.py +375 -0
  217. mobile_agent/devkit/links_cli.py +123 -0
  218. mobile_agent/devkit/links_routes.py +83 -0
  219. mobile_agent/devkit/nightly.py +152 -0
  220. mobile_agent/devkit/shots.py +203 -0
  221. mobile_agent/devkit/testflight.py +155 -0
  222. mobile_agent/devkit/testflight_cli.py +127 -0
  223. mobile_agent/devkit/testflight_routes.py +111 -0
  224. mobile_agent/devkit/xcode.py +236 -0
  225. mobile_agent/devkit/xcode_cli.py +101 -0
  226. mobile_agent/devtools.py +419 -0
  227. mobile_agent/doctor.py +509 -0
  228. mobile_agent/doctor_fix.py +467 -0
  229. mobile_agent/downloads.py +55 -0
  230. mobile_agent/drivers.py +793 -0
  231. mobile_agent/dry_run.py +102 -0
  232. mobile_agent/effect_ledger.py +83 -0
  233. mobile_agent/engine_health.py +378 -0
  234. mobile_agent/engine_hooks.py +96 -0
  235. mobile_agent/engines.py +408 -0
  236. mobile_agent/env_file.py +63 -0
  237. mobile_agent/env_secrets.py +149 -0
  238. mobile_agent/errors.py +15 -0
  239. mobile_agent/evals/__init__.py +3 -0
  240. mobile_agent/evals/app_survey.py +149 -0
  241. mobile_agent/evals/development/__init__.py +1 -0
  242. mobile_agent/evals/development/score.py +276 -0
  243. mobile_agent/evals/latency_report.py +312 -0
  244. mobile_agent/evals/offline_report.py +374 -0
  245. mobile_agent/evals/oracles.py +298 -0
  246. mobile_agent/evals/redteam/__init__.py +15 -0
  247. mobile_agent/evals/redteam/__main__.py +72 -0
  248. mobile_agent/evals/redteam/cases.py +730 -0
  249. mobile_agent/evals/redteam/harness.py +604 -0
  250. mobile_agent/evals/redteam/live.py +139 -0
  251. mobile_agent/evals/redteam/plain.py +85 -0
  252. mobile_agent/evals/vision/__init__.py +0 -0
  253. mobile_agent/evals/vision/images/dog_001.jpg +0 -0
  254. mobile_agent/evals/vision/images/dog_002.jpg +0 -0
  255. mobile_agent/evals/vision/images/dog_003.jpg +0 -0
  256. mobile_agent/evals/vision/images/dog_004.jpg +0 -0
  257. mobile_agent/evals/vision/images/dog_005.jpg +0 -0
  258. mobile_agent/evals/vision/images/dog_006.jpg +0 -0
  259. mobile_agent/evals/vision/images/dog_007.jpg +0 -0
  260. mobile_agent/evals/vision/images/dog_010.jpg +0 -0
  261. mobile_agent/evals/vision/images/dog_011.jpg +0 -0
  262. mobile_agent/evals/vision/images/dog_013.jpg +0 -0
  263. mobile_agent/evals/vision/images/dog_014.jpg +0 -0
  264. mobile_agent/evals/vision/images/dog_015.jpg +0 -0
  265. mobile_agent/evals/vision/images/dog_016.jpg +0 -0
  266. mobile_agent/evals/vision/images/dog_017.jpg +0 -0
  267. mobile_agent/evals/vision/images/dog_019.jpg +0 -0
  268. mobile_agent/evals/vision/images/dog_020.jpg +0 -0
  269. mobile_agent/evals/vision/images/dog_021.jpg +0 -0
  270. mobile_agent/evals/vision/images/dog_022.jpg +0 -0
  271. mobile_agent/evals/vision/images/dog_023.jpg +0 -0
  272. mobile_agent/evals/vision/images/dog_024.jpg +0 -0
  273. mobile_agent/evals/vision/images/dog_026.jpg +0 -0
  274. mobile_agent/evals/vision/images/dog_027.jpg +0 -0
  275. mobile_agent/evals/vision/images/dog_034.jpg +0 -0
  276. mobile_agent/evals/vision/images/dog_035.jpg +0 -0
  277. mobile_agent/evals/vision/images/dog_038.jpg +0 -0
  278. mobile_agent/evals/vision/images/dog_042.jpg +0 -0
  279. mobile_agent/evals/vision/images/dog_047.jpg +0 -0
  280. mobile_agent/evals/vision/images/dog_052.jpg +0 -0
  281. mobile_agent/evals/vision/images/dog_058.jpg +0 -0
  282. mobile_agent/evals/vision/images/dog_062.jpg +0 -0
  283. mobile_agent/evals/vision/images/eyes_000.jpg +0 -0
  284. mobile_agent/evals/vision/images/eyes_010.jpg +0 -0
  285. mobile_agent/evals/vision/images/eyes_011.jpg +0 -0
  286. mobile_agent/evals/vision/images/eyes_015.jpg +0 -0
  287. mobile_agent/evals/vision/images/eyes_016.jpg +0 -0
  288. mobile_agent/evals/vision/images/eyes_017.jpg +0 -0
  289. mobile_agent/evals/vision/images/eyes_019.jpg +0 -0
  290. mobile_agent/evals/vision/images/eyes_021.jpg +0 -0
  291. mobile_agent/evals/vision/images/eyes_025.jpg +0 -0
  292. mobile_agent/evals/vision/images/eyes_026.jpg +0 -0
  293. mobile_agent/evals/vision/images/eyes_027.jpg +0 -0
  294. mobile_agent/evals/vision/images/eyes_030.jpg +0 -0
  295. mobile_agent/evals/vision/images/eyes_032.jpg +0 -0
  296. mobile_agent/evals/vision/images/eyes_033.jpg +0 -0
  297. mobile_agent/evals/vision/images/eyes_034.jpg +0 -0
  298. mobile_agent/evals/vision/images/eyes_035.jpg +0 -0
  299. mobile_agent/evals/vision/images/eyes_036.jpg +0 -0
  300. mobile_agent/evals/vision/images/eyes_037.jpg +0 -0
  301. mobile_agent/evals/vision/images/eyes_039.jpg +0 -0
  302. mobile_agent/evals/vision/images/eyes_040.jpg +0 -0
  303. mobile_agent/evals/vision/images/eyes_041.jpg +0 -0
  304. mobile_agent/evals/vision/images/eyes_042.jpg +0 -0
  305. mobile_agent/evals/vision/images/eyes_043.jpg +0 -0
  306. mobile_agent/evals/vision/images/eyes_044.jpg +0 -0
  307. mobile_agent/evals/vision/images/eyes_046.jpg +0 -0
  308. mobile_agent/evals/vision/images/eyes_047.jpg +0 -0
  309. mobile_agent/evals/vision/images/eyes_048.jpg +0 -0
  310. mobile_agent/evals/vision/images/eyes_049.jpg +0 -0
  311. mobile_agent/evals/vision/images/eyes_051.jpg +0 -0
  312. mobile_agent/evals/vision/images/eyes_053.jpg +0 -0
  313. mobile_agent/evals/vision/images/eyes_054.jpg +0 -0
  314. mobile_agent/evals/vision/images/eyes_055.jpg +0 -0
  315. mobile_agent/evals/vision/images/eyes_058.jpg +0 -0
  316. mobile_agent/evals/vision/images/eyes_059.jpg +0 -0
  317. mobile_agent/evals/vision/images/receipt_002.jpg +0 -0
  318. mobile_agent/evals/vision/images/receipt_003.jpg +0 -0
  319. mobile_agent/evals/vision/images/receipt_004.jpg +0 -0
  320. mobile_agent/evals/vision/images/receipt_007.jpg +0 -0
  321. mobile_agent/evals/vision/images/receipt_009.jpg +0 -0
  322. mobile_agent/evals/vision/images/receipt_010.jpg +0 -0
  323. mobile_agent/evals/vision/images/receipt_011.jpg +0 -0
  324. mobile_agent/evals/vision/images/receipt_012.jpg +0 -0
  325. mobile_agent/evals/vision/images/receipt_013.jpg +0 -0
  326. mobile_agent/evals/vision/images/receipt_014.jpg +0 -0
  327. mobile_agent/evals/vision/images/receipt_016.jpg +0 -0
  328. mobile_agent/evals/vision/images/receipt_017.jpg +0 -0
  329. mobile_agent/evals/vision/images/receipt_023.jpg +0 -0
  330. mobile_agent/evals/vision/images/receipt_027.jpg +0 -0
  331. mobile_agent/evals/vision/images/receipt_029.jpg +0 -0
  332. mobile_agent/evals/vision/images/receipt_032.jpg +0 -0
  333. mobile_agent/evals/vision/images/receipt_033.jpg +0 -0
  334. mobile_agent/evals/vision/images/receipt_036.jpg +0 -0
  335. mobile_agent/evals/vision/images/receipt_041.jpg +0 -0
  336. mobile_agent/evals/vision/images/receipt_043.jpg +0 -0
  337. mobile_agent/evals/vision/images/receipt_047.jpg +0 -0
  338. mobile_agent/evals/vision/images/receipt_049.jpg +0 -0
  339. mobile_agent/evals/vision/images/receipt_051.jpg +0 -0
  340. mobile_agent/evals/vision/images/receipt_053.jpg +0 -0
  341. mobile_agent/evals/vision/images/receipt_056.jpg +0 -0
  342. mobile_agent/evals/vision/images/receipt_057.jpg +0 -0
  343. mobile_agent/evals/vision/images/receipt_058.jpg +0 -0
  344. mobile_agent/evals/vision/images/receipt_059.jpg +0 -0
  345. mobile_agent/evals/vision/images/receipt_060.jpg +0 -0
  346. mobile_agent/evals/vision/images/receipt_061.jpg +0 -0
  347. mobile_agent/evals/vision/images/receipt_062.jpg +0 -0
  348. mobile_agent/evals/vision/images/receipt_063.jpg +0 -0
  349. mobile_agent/evals/vision/images/receipt_064.jpg +0 -0
  350. mobile_agent/evals/vision/images/receipt_065.jpg +0 -0
  351. mobile_agent/evals/vision/images/receipt_066.jpg +0 -0
  352. mobile_agent/evals/vision/images/receipt_067.jpg +0 -0
  353. mobile_agent/evals/vision/images/receipt_068.jpg +0 -0
  354. mobile_agent/evals/vision/images/receipt_069.jpg +0 -0
  355. mobile_agent/evals/vision/images/receipt_070.jpg +0 -0
  356. mobile_agent/evals/vision/images/receipt_071.jpg +0 -0
  357. mobile_agent/evals/vision/images/sunglasses_000.jpg +0 -0
  358. mobile_agent/evals/vision/images/sunglasses_001.jpg +0 -0
  359. mobile_agent/evals/vision/images/sunglasses_002.jpg +0 -0
  360. mobile_agent/evals/vision/images/sunglasses_004.jpg +0 -0
  361. mobile_agent/evals/vision/images/sunglasses_005.jpg +0 -0
  362. mobile_agent/evals/vision/images/sunglasses_008.jpg +0 -0
  363. mobile_agent/evals/vision/images/sunglasses_009.jpg +0 -0
  364. mobile_agent/evals/vision/images/sunglasses_010.jpg +0 -0
  365. mobile_agent/evals/vision/images/sunglasses_012.jpg +0 -0
  366. mobile_agent/evals/vision/images/sunglasses_013.jpg +0 -0
  367. mobile_agent/evals/vision/images/sunglasses_015.jpg +0 -0
  368. mobile_agent/evals/vision/images/sunglasses_016.jpg +0 -0
  369. mobile_agent/evals/vision/images/sunglasses_017.jpg +0 -0
  370. mobile_agent/evals/vision/images/sunglasses_018.jpg +0 -0
  371. mobile_agent/evals/vision/images/sunglasses_021.jpg +0 -0
  372. mobile_agent/evals/vision/images/sunglasses_022.jpg +0 -0
  373. mobile_agent/evals/vision/images/sunglasses_023.jpg +0 -0
  374. mobile_agent/evals/vision/images/sunglasses_024.jpg +0 -0
  375. mobile_agent/evals/vision/images/sunglasses_030.jpg +0 -0
  376. mobile_agent/evals/vision/images/sunglasses_031.jpg +0 -0
  377. mobile_agent/evals/vision/images/sunglasses_032.jpg +0 -0
  378. mobile_agent/evals/vision/images/sunglasses_033.jpg +0 -0
  379. mobile_agent/evals/vision/images/sunglasses_034.jpg +0 -0
  380. mobile_agent/evals/vision/images/sunglasses_036.jpg +0 -0
  381. mobile_agent/evals/vision/images/sunglasses_038.jpg +0 -0
  382. mobile_agent/evals/vision/images/sunglasses_039.jpg +0 -0
  383. mobile_agent/evals/vision/images/sunglasses_040.jpg +0 -0
  384. mobile_agent/evals/vision/images/sunglasses_041.jpg +0 -0
  385. mobile_agent/evals/vision/manifest.json +2355 -0
  386. mobile_agent/eventbus.py +110 -0
  387. mobile_agent/explain.py +34 -0
  388. mobile_agent/failure_line.py +197 -0
  389. mobile_agent/fanout.py +663 -0
  390. mobile_agent/feedback/__init__.py +2 -0
  391. mobile_agent/feedback/bundle.py +204 -0
  392. mobile_agent/feedback/outbox.py +196 -0
  393. mobile_agent/feedback/register.py +9 -0
  394. mobile_agent/feedback/routes.py +177 -0
  395. mobile_agent/fleet.py +841 -0
  396. mobile_agent/fleet_health.py +729 -0
  397. mobile_agent/fleet_phone.py +186 -0
  398. mobile_agent/frontier/__init__.py +79 -0
  399. mobile_agent/frontier/act.py +380 -0
  400. mobile_agent/frontier/actions.py +70 -0
  401. mobile_agent/frontier/agent.py +256 -0
  402. mobile_agent/frontier/approvals.py +366 -0
  403. mobile_agent/frontier/hooks.py +199 -0
  404. mobile_agent/frontier/interrupts.py +125 -0
  405. mobile_agent/frontier/observe.py +331 -0
  406. mobile_agent/frontier/phases.py +381 -0
  407. mobile_agent/frontier/pipeline.py +61 -0
  408. mobile_agent/frontier/prompt.py +384 -0
  409. mobile_agent/frontier/protocol.py +91 -0
  410. mobile_agent/frontier/safety.py +467 -0
  411. mobile_agent/frontier/saved.py +375 -0
  412. mobile_agent/frontier/screen.py +419 -0
  413. mobile_agent/frontier/skills.py +147 -0
  414. mobile_agent/frontier/stages/__init__.py +1 -0
  415. mobile_agent/frontier/stages/after.py +192 -0
  416. mobile_agent/frontier/stages/approve.py +138 -0
  417. mobile_agent/frontier/stages/controls.py +115 -0
  418. mobile_agent/frontier/stages/ends.py +102 -0
  419. mobile_agent/frontier/stages/gate.py +92 -0
  420. mobile_agent/frontier/stages/guards.py +132 -0
  421. mobile_agent/frontier/stages/points.py +155 -0
  422. mobile_agent/frontier/stages/send.py +177 -0
  423. mobile_agent/frontier/turn.py +151 -0
  424. mobile_agent/full_disk_access.py +83 -0
  425. mobile_agent/gemini.py +69 -0
  426. mobile_agent/genuine.py +120 -0
  427. mobile_agent/graph/__init__.py +4 -0
  428. mobile_agent/graph/catalog.py +789 -0
  429. mobile_agent/graph/chain.py +306 -0
  430. mobile_agent/graph/dispatch.py +98 -0
  431. mobile_agent/graph/draft.py +373 -0
  432. mobile_agent/graph/edit.py +76 -0
  433. mobile_agent/graph/estimate.py +189 -0
  434. mobile_agent/graph/expr.py +562 -0
  435. mobile_agent/graph/functions.py +271 -0
  436. mobile_agent/graph/infer.py +144 -0
  437. mobile_agent/graph/nodes.py +936 -0
  438. mobile_agent/graph/phone_steps.py +178 -0
  439. mobile_agent/graph/rerun.py +327 -0
  440. mobile_agent/graph/run.py +899 -0
  441. mobile_agent/graph/schema.py +842 -0
  442. mobile_agent/graph/state.py +161 -0
  443. mobile_agent/graph/values.py +198 -0
  444. mobile_agent/harness/__init__.py +2 -0
  445. mobile_agent/harness/checkpoints.py +468 -0
  446. mobile_agent/harness/control.py +101 -0
  447. mobile_agent/harness/prewarm.py +213 -0
  448. mobile_agent/harness/register.py +43 -0
  449. mobile_agent/harness/routes.py +114 -0
  450. mobile_agent/harness/settings.py +66 -0
  451. mobile_agent/harness/tools/__init__.py +67 -0
  452. mobile_agent/harness/tools/ask_user.py +102 -0
  453. mobile_agent/harness/tools/watch.py +120 -0
  454. mobile_agent/harness_api.py +815 -0
  455. mobile_agent/hedge.py +203 -0
  456. mobile_agent/help_topics.py +305 -0
  457. mobile_agent/hints.py +248 -0
  458. mobile_agent/history.py +69 -0
  459. mobile_agent/home.py +214 -0
  460. mobile_agent/http_pool.py +175 -0
  461. mobile_agent/image_policy.py +139 -0
  462. mobile_agent/inbox.py +148 -0
  463. mobile_agent/installed_apps.py +344 -0
  464. mobile_agent/integrations/__init__.py +1 -0
  465. mobile_agent/integrations/cli.py +472 -0
  466. mobile_agent/integrations/client_files.py +65 -0
  467. mobile_agent/integrations/clients.py +998 -0
  468. mobile_agent/integrations/doctor.py +185 -0
  469. mobile_agent/integrations/jsonc.py +383 -0
  470. mobile_agent/integrations/routes.py +135 -0
  471. mobile_agent/integrations/rules.py +112 -0
  472. mobile_agent/integrations/server_options.py +46 -0
  473. mobile_agent/integrations/skill.py +152 -0
  474. mobile_agent/integrations/skill_files.py +10 -0
  475. mobile_agent/integrations/textedit.py +309 -0
  476. mobile_agent/intents/__init__.py +17 -0
  477. mobile_agent/intents/atlas.py +216 -0
  478. mobile_agent/intents/callback.py +44 -0
  479. mobile_agent/intents/cli.py +55 -0
  480. mobile_agent/intents/harvest.py +91 -0
  481. mobile_agent/intents/plan.py +93 -0
  482. mobile_agent/ios/__init__.py +1 -0
  483. mobile_agent/ios/guide.py +16 -0
  484. mobile_agent/ios/kb.py +212 -0
  485. mobile_agent/ios/kb_data.py +1588 -0
  486. mobile_agent/ios/mcp.py +44 -0
  487. mobile_agent/ios/register.py +8 -0
  488. mobile_agent/ios/system.py +369 -0
  489. mobile_agent/journal.py +531 -0
  490. mobile_agent/keychain_acl.py +374 -0
  491. mobile_agent/keys.py +395 -0
  492. mobile_agent/latency_overlap.py +36 -0
  493. mobile_agent/latency_trace.py +170 -0
  494. mobile_agent/learned/__init__.py +15 -0
  495. mobile_agent/learned/app_path.py +160 -0
  496. mobile_agent/learned/code_read.py +38 -0
  497. mobile_agent/learned/parts.py +546 -0
  498. mobile_agent/learned/programs.py +691 -0
  499. mobile_agent/learned/register.py +171 -0
  500. mobile_agent/learned/routes.py +61 -0
  501. mobile_agent/learned/taught.py +329 -0
  502. mobile_agent/learned/teach.py +538 -0
  503. mobile_agent/learned/workflows.py +647 -0
  504. mobile_agent/learned/zero.py +41 -0
  505. mobile_agent/links.py +37 -0
  506. mobile_agent/listen.py +607 -0
  507. mobile_agent/lock_spike.py +278 -0
  508. mobile_agent/lockscreen.py +526 -0
  509. mobile_agent/login.py +354 -0
  510. mobile_agent/longform/__init__.py +15 -0
  511. mobile_agent/longform/ads.py +191 -0
  512. mobile_agent/longform/blocks.py +117 -0
  513. mobile_agent/longform/budget.py +265 -0
  514. mobile_agent/longform/context.py +285 -0
  515. mobile_agent/longform/costs.py +93 -0
  516. mobile_agent/longform/digest.py +145 -0
  517. mobile_agent/longform/extract.py +508 -0
  518. mobile_agent/longform/feed.py +505 -0
  519. mobile_agent/longform/followup.py +168 -0
  520. mobile_agent/longform/frames.py +82 -0
  521. mobile_agent/longform/identity.py +87 -0
  522. mobile_agent/longform/inbox.py +221 -0
  523. mobile_agent/longform/items.py +425 -0
  524. mobile_agent/longform/models.py +115 -0
  525. mobile_agent/longform/notebook.py +329 -0
  526. mobile_agent/longform/plan.py +614 -0
  527. mobile_agent/longform/recall.py +195 -0
  528. mobile_agent/longform/receipts.py +81 -0
  529. mobile_agent/longform/register.py +124 -0
  530. mobile_agent/longform/report.py +343 -0
  531. mobile_agent/longform/routes.py +102 -0
  532. mobile_agent/longform/session.py +644 -0
  533. mobile_agent/longform/store.py +146 -0
  534. mobile_agent/longform/synthesis.py +347 -0
  535. mobile_agent/mac_tools.py +223 -0
  536. mobile_agent/manual_control.py +143 -0
  537. mobile_agent/maps.py +149 -0
  538. mobile_agent/marks.py +92 -0
  539. mobile_agent/mcp_server/__init__.py +3 -0
  540. mobile_agent/mcp_server/__main__.py +29 -0
  541. mobile_agent/mcp_server/action_journal.py +24 -0
  542. mobile_agent/mcp_server/action_tools.py +638 -0
  543. mobile_agent/mcp_server/approvals.py +219 -0
  544. mobile_agent/mcp_server/catalog.py +92 -0
  545. mobile_agent/mcp_server/cli.py +194 -0
  546. mobile_agent/mcp_server/codes.py +147 -0
  547. mobile_agent/mcp_server/device_tools.py +306 -0
  548. mobile_agent/mcp_server/direct.py +272 -0
  549. mobile_agent/mcp_server/expo_tools.py +105 -0
  550. mobile_agent/mcp_server/phone_tools.py +510 -0
  551. mobile_agent/mcp_server/protocol.py +364 -0
  552. mobile_agent/mcp_server/registry.py +55 -0
  553. mobile_agent/mcp_server/run_tools.py +531 -0
  554. mobile_agent/mcp_server/schemas.py +425 -0
  555. mobile_agent/mcp_server/screen_tools.py +168 -0
  556. mobile_agent/mcp_server/session.py +469 -0
  557. mobile_agent/mcp_server/support.py +145 -0
  558. mobile_agent/mcp_server/tools.py +493 -0
  559. mobile_agent/memory/__init__.py +9 -0
  560. mobile_agent/memory/cli.py +207 -0
  561. mobile_agent/memory/extract.py +191 -0
  562. mobile_agent/memory/listener.py +100 -0
  563. mobile_agent/memory/model.py +144 -0
  564. mobile_agent/memory/project.py +96 -0
  565. mobile_agent/memory/provider.py +127 -0
  566. mobile_agent/memory/register.py +21 -0
  567. mobile_agent/memory/retrieval.py +172 -0
  568. mobile_agent/memory/routes.py +216 -0
  569. mobile_agent/memory/runs.py +124 -0
  570. mobile_agent/memory/store.py +806 -0
  571. mobile_agent/memory/writer.py +278 -0
  572. mobile_agent/mirroring.py +585 -0
  573. mobile_agent/mirroring_runtime.py +221 -0
  574. mobile_agent/model_clients.py +604 -0
  575. mobile_agent/narrate.py +813 -0
  576. mobile_agent/native_helpers.py +84 -0
  577. mobile_agent/netxray/__init__.py +1 -0
  578. mobile_agent/netxray/analyze.py +322 -0
  579. mobile_agent/netxray/attribution.py +62 -0
  580. mobile_agent/netxray/capture.py +88 -0
  581. mobile_agent/netxray/cli.py +142 -0
  582. mobile_agent/netxray/job.py +219 -0
  583. mobile_agent/netxray/mobster-pcap.c +560 -0
  584. mobile_agent/netxray/packets.py +410 -0
  585. mobile_agent/netxray/pcapng.py +223 -0
  586. mobile_agent/netxray/register.py +93 -0
  587. mobile_agent/netxray/report.py +214 -0
  588. mobile_agent/netxray/store.py +48 -0
  589. mobile_agent/netxray/summary.py +157 -0
  590. mobile_agent/netxray/target.py +67 -0
  591. mobile_agent/netxray/tool.py +66 -0
  592. mobile_agent/netxray/trackers.py +194 -0
  593. mobile_agent/notify_triggers/__init__.py +1 -0
  594. mobile_agent/notify_triggers/rules.py +39 -0
  595. mobile_agent/notify_triggers/sources.py +315 -0
  596. mobile_agent/notify_triggers/watch.py +135 -0
  597. mobile_agent/offers.py +86 -0
  598. mobile_agent/orchestrate.py +215 -0
  599. mobile_agent/outlets/__init__.py +1 -0
  600. mobile_agent/outlets/connect.py +249 -0
  601. mobile_agent/outlets/folder.py +180 -0
  602. mobile_agent/outlets/hooks.py +453 -0
  603. mobile_agent/outlets/inbound.py +472 -0
  604. mobile_agent/outlets/inbound_reply.py +57 -0
  605. mobile_agent/outlets/inbound_routes.py +81 -0
  606. mobile_agent/outlets/kinds.py +308 -0
  607. mobile_agent/outlets/mail.py +110 -0
  608. mobile_agent/outlets/net.py +86 -0
  609. mobile_agent/outlets/notion.py +244 -0
  610. mobile_agent/outlets/register.py +56 -0
  611. mobile_agent/outlets/routes.py +135 -0
  612. mobile_agent/outlets/service.py +580 -0
  613. mobile_agent/outlets/sheets.py +169 -0
  614. mobile_agent/outlets/slack.py +177 -0
  615. mobile_agent/outlets/store.py +194 -0
  616. mobile_agent/outlets/vault.py +80 -0
  617. mobile_agent/outlets/webhook.py +316 -0
  618. mobile_agent/output_contract.py +141 -0
  619. mobile_agent/owner_presence.py +211 -0
  620. mobile_agent/ownership.py +219 -0
  621. mobile_agent/packs/__init__.py +1 -0
  622. mobile_agent/packs/calendar.py +243 -0
  623. mobile_agent/packs/context.py +68 -0
  624. mobile_agent/packs/core.py +757 -0
  625. mobile_agent/packs/front_door.py +316 -0
  626. mobile_agent/packs/heal.py +214 -0
  627. mobile_agent/packs/health.py +122 -0
  628. mobile_agent/packs/insights.py +76 -0
  629. mobile_agent/packs/instagram.py +640 -0
  630. mobile_agent/packs/linkedin.py +106 -0
  631. mobile_agent/packs/maps.py +196 -0
  632. mobile_agent/packs/mcp.py +149 -0
  633. mobile_agent/packs/messages.py +270 -0
  634. mobile_agent/packs/notes.py +226 -0
  635. mobile_agent/packs/photos.py +166 -0
  636. mobile_agent/packs/pins.py +47 -0
  637. mobile_agent/packs/reddit.py +110 -0
  638. mobile_agent/packs/register.py +276 -0
  639. mobile_agent/packs/rows.py +108 -0
  640. mobile_agent/packs/safari.py +143 -0
  641. mobile_agent/packs/settings.py +253 -0
  642. mobile_agent/packs/social.py +300 -0
  643. mobile_agent/packs/threads.py +97 -0
  644. mobile_agent/packs/tiktok.py +455 -0
  645. mobile_agent/packs/tools.py +290 -0
  646. mobile_agent/packs/x.py +127 -0
  647. mobile_agent/packs/youtube.py +199 -0
  648. mobile_agent/parity.py +270 -0
  649. mobile_agent/paths.py +99 -0
  650. mobile_agent/perception.py +470 -0
  651. mobile_agent/permissions/__init__.py +32 -0
  652. mobile_agent/permissions/consequence.py +535 -0
  653. mobile_agent/permissions/exceptions.py +99 -0
  654. mobile_agent/permissions/gate.py +316 -0
  655. mobile_agent/permissions/outside.py +316 -0
  656. mobile_agent/permissions/policy.py +288 -0
  657. mobile_agent/permissions/project_policy.py +411 -0
  658. mobile_agent/permissions/register.py +148 -0
  659. mobile_agent/permissions/rules.py +291 -0
  660. mobile_agent/phone_io/__init__.py +146 -0
  661. mobile_agent/phone_io/cli.py +427 -0
  662. mobile_agent/phone_io/clipboard.py +78 -0
  663. mobile_agent/phone_io/controls.py +78 -0
  664. mobile_agent/phone_io/files.py +289 -0
  665. mobile_agent/phone_io/handoff.py +123 -0
  666. mobile_agent/phone_io/icloud.py +118 -0
  667. mobile_agent/phone_io/install.py +188 -0
  668. mobile_agent/phone_io/network_capture.py +188 -0
  669. mobile_agent/phone_io/network_cli.py +138 -0
  670. mobile_agent/phone_io/network_profiles.py +138 -0
  671. mobile_agent/phone_io/photos.py +512 -0
  672. mobile_agent/phone_io/recording.py +115 -0
  673. mobile_agent/phone_io/secrets.py +146 -0
  674. mobile_agent/phone_io/transfer.py +895 -0
  675. mobile_agent/phone_io/tunnel.py +129 -0
  676. mobile_agent/phone_ready.py +91 -0
  677. mobile_agent/pinning.py +158 -0
  678. mobile_agent/plan_gate.py +807 -0
  679. mobile_agent/plugins/__init__.py +3 -0
  680. mobile_agent/plugins/install.py +163 -0
  681. mobile_agent/plugins/manifest.py +242 -0
  682. mobile_agent/plugins/register.py +15 -0
  683. mobile_agent/plugins/routes.py +82 -0
  684. mobile_agent/plugins/service.py +322 -0
  685. mobile_agent/plugins/skills.py +76 -0
  686. mobile_agent/plugins/store.py +79 -0
  687. mobile_agent/pool.py +285 -0
  688. mobile_agent/power_watch.py +275 -0
  689. mobile_agent/premium_model.py +22 -0
  690. mobile_agent/project_env.py +291 -0
  691. mobile_agent/project_env_cli.py +74 -0
  692. mobile_agent/project_env_routes.py +55 -0
  693. mobile_agent/projects/__init__.py +27 -0
  694. mobile_agent/projects/ci.py +332 -0
  695. mobile_agent/projects/clone.py +339 -0
  696. mobile_agent/projects/drafts.py +158 -0
  697. mobile_agent/projects/folder.py +436 -0
  698. mobile_agent/projects/github.py +131 -0
  699. mobile_agent/projects/register.py +31 -0
  700. mobile_agent/projects/revisions.py +253 -0
  701. mobile_agent/projects/routes.py +234 -0
  702. mobile_agent/projects/runs.py +297 -0
  703. mobile_agent/projects/service.py +695 -0
  704. mobile_agent/projects/setup.py +84 -0
  705. mobile_agent/projects/store.py +379 -0
  706. mobile_agent/projects/sync.py +380 -0
  707. mobile_agent/projects/workflow_files.py +270 -0
  708. mobile_agent/replay_core/__init__.py +26 -0
  709. mobile_agent/replay_core/checks.py +594 -0
  710. mobile_agent/replay_core/engine.py +596 -0
  711. mobile_agent/replay_core/heal.py +364 -0
  712. mobile_agent/replay_core/path.py +240 -0
  713. mobile_agent/replay_core/recorder.py +206 -0
  714. mobile_agent/replay_core/store.py +171 -0
  715. mobile_agent/result_blocks.py +941 -0
  716. mobile_agent/run.py +642 -0
  717. mobile_agent/run_delivery.py +123 -0
  718. mobile_agent/run_executor.py +200 -0
  719. mobile_agent/run_export.py +284 -0
  720. mobile_agent/run_gif.py +891 -0
  721. mobile_agent/runner_hints.py +86 -0
  722. mobile_agent/runner_renewal.py +205 -0
  723. mobile_agent/runtime_create.py +549 -0
  724. mobile_agent/runtime_driver.py +458 -0
  725. mobile_agent/runtime_limits.py +319 -0
  726. mobile_agent/runtime_queue.py +394 -0
  727. mobile_agent/runtime_runs.py +383 -0
  728. mobile_agent/runtime_status.py +181 -0
  729. mobile_agent/runtime_teach.py +295 -0
  730. mobile_agent/runtime_work.py +599 -0
  731. mobile_agent/screen_text.py +101 -0
  732. mobile_agent/secret_filter.py +113 -0
  733. mobile_agent/serve.py +160 -0
  734. mobile_agent/server.py +363 -0
  735. mobile_agent/service_support.py +42 -0
  736. mobile_agent/settle/__init__.py +1 -0
  737. mobile_agent/settle/predict.py +102 -0
  738. mobile_agent/settle/replay.py +92 -0
  739. mobile_agent/settle/store.py +93 -0
  740. mobile_agent/settle/stream.py +43 -0
  741. mobile_agent/setup_cli.py +225 -0
  742. mobile_agent/setup_errors.py +87 -0
  743. mobile_agent/setup_service.py +527 -0
  744. mobile_agent/share.py +104 -0
  745. mobile_agent/signing.py +184 -0
  746. mobile_agent/sim/__init__.py +6 -0
  747. mobile_agent/sim/__main__.py +26 -0
  748. mobile_agent/sim/api.py +952 -0
  749. mobile_agent/sim/apps.py +172 -0
  750. mobile_agent/sim/biometrics.py +84 -0
  751. mobile_agent/sim/cli.py +271 -0
  752. mobile_agent/sim/doctor.py +293 -0
  753. mobile_agent/sim/governor.py +90 -0
  754. mobile_agent/sim/holders.py +88 -0
  755. mobile_agent/sim/models.py +62 -0
  756. mobile_agent/sim/registry.py +161 -0
  757. mobile_agent/sim/simctl.py +269 -0
  758. mobile_agent/sim/wda.py +618 -0
  759. mobile_agent/sim/xcode.py +74 -0
  760. mobile_agent/skills/__init__.py +12 -0
  761. mobile_agent/skills/codes.py +597 -0
  762. mobile_agent/skills/notifications.py +281 -0
  763. mobile_agent/skills/open_url.py +127 -0
  764. mobile_agent/smart_events.py +588 -0
  765. mobile_agent/smart_result.py +490 -0
  766. mobile_agent/smart_run.py +413 -0
  767. mobile_agent/state.py +662 -0
  768. mobile_agent/states.py +217 -0
  769. mobile_agent/steering.py +70 -0
  770. mobile_agent/style.py +178 -0
  771. mobile_agent/supervised.py +234 -0
  772. mobile_agent/swift/helper_bt_hid.swift +534 -0
  773. mobile_agent/swift/helper_doc_text.swift +184 -0
  774. mobile_agent/swift/helper_draft_reply.swift +170 -0
  775. mobile_agent/swift/helper_map.swift +110 -0
  776. mobile_agent/swift/helper_mirroring.swift +1099 -0
  777. mobile_agent/swift/helper_mjpeg_mp4.swift +103 -0
  778. mobile_agent/swift/helper_pdf.swift +100 -0
  779. mobile_agent/swift/helper_phone_audio.swift +993 -0
  780. mobile_agent/swift/helper_phone_screen.swift +416 -0
  781. mobile_agent/swift/helper_screen_text.swift +24 -0
  782. mobile_agent/swift/helper_transfer.swift +246 -0
  783. mobile_agent/swift/ocr.swift +22 -0
  784. mobile_agent/task_policy.py +337 -0
  785. mobile_agent/team_signing/__init__.py +34 -0
  786. mobile_agent/team_signing/apple_ca.py +37 -0
  787. mobile_agent/team_signing/bundle.py +136 -0
  788. mobile_agent/team_signing/cli.py +208 -0
  789. mobile_agent/team_signing/identity.py +248 -0
  790. mobile_agent/team_signing/ledger.py +105 -0
  791. mobile_agent/team_signing/prebuilt.py +61 -0
  792. mobile_agent/team_signing/resign.py +256 -0
  793. mobile_agent/team_signing/service.py +382 -0
  794. mobile_agent/team_signing/tooling.py +243 -0
  795. mobile_agent/team_signing/writes.py +268 -0
  796. mobile_agent/terminal_image.py +81 -0
  797. mobile_agent/testkit/__init__.py +1 -0
  798. mobile_agent/testkit/a11y_baseline.py +132 -0
  799. mobile_agent/testkit/api.py +403 -0
  800. mobile_agent/testkit/capsule.py +400 -0
  801. mobile_agent/testkit/capsule_cli.py +57 -0
  802. mobile_agent/testkit/cli.py +948 -0
  803. mobile_agent/testkit/cli_arguments.py +193 -0
  804. mobile_agent/testkit/comparison.py +288 -0
  805. mobile_agent/testkit/comparison_cli.py +41 -0
  806. mobile_agent/testkit/discover.py +257 -0
  807. mobile_agent/testkit/editor.py +243 -0
  808. mobile_agent/testkit/execute.py +515 -0
  809. mobile_agent/testkit/github_checks.py +248 -0
  810. mobile_agent/testkit/gitref.py +66 -0
  811. mobile_agent/testkit/heal_review.py +176 -0
  812. mobile_agent/testkit/health.py +480 -0
  813. mobile_agent/testkit/history.py +255 -0
  814. mobile_agent/testkit/hosted_results.py +321 -0
  815. mobile_agent/testkit/input_files.py +173 -0
  816. mobile_agent/testkit/jobs.py +285 -0
  817. mobile_agent/testkit/junit_verdict.py +33 -0
  818. mobile_agent/testkit/maestro.py +802 -0
  819. mobile_agent/testkit/mcp.py +562 -0
  820. mobile_agent/testkit/mcp_comparison.py +88 -0
  821. mobile_agent/testkit/minimize.py +471 -0
  822. mobile_agent/testkit/minimizer_cli.py +48 -0
  823. mobile_agent/testkit/notify.py +212 -0
  824. mobile_agent/testkit/phone_video.py +191 -0
  825. mobile_agent/testkit/pr.py +491 -0
  826. mobile_agent/testkit/pr_report.py +562 -0
  827. mobile_agent/testkit/prbuild.py +148 -0
  828. mobile_agent/testkit/proof_report.py +55 -0
  829. mobile_agent/testkit/provenance.py +119 -0
  830. mobile_agent/testkit/prrun.py +244 -0
  831. mobile_agent/testkit/recent.py +166 -0
  832. mobile_agent/testkit/recipes.py +146 -0
  833. mobile_agent/testkit/record.py +204 -0
  834. mobile_agent/testkit/register.py +288 -0
  835. mobile_agent/testkit/reports.py +998 -0
  836. mobile_agent/testkit/repro.py +432 -0
  837. mobile_agent/testkit/scenario.py +440 -0
  838. mobile_agent/testkit/scenario_cli.py +37 -0
  839. mobile_agent/testkit/snapshots_cli.py +60 -0
  840. mobile_agent/testkit/suite.py +670 -0
  841. mobile_agent/testkit/targets.py +497 -0
  842. mobile_agent/testkit/xctest.py +378 -0
  843. mobile_agent/threads/__init__.py +1 -0
  844. mobile_agent/threads/chat_once.py +264 -0
  845. mobile_agent/threads/cli.py +740 -0
  846. mobile_agent/threads/client.py +326 -0
  847. mobile_agent/threads/context.py +246 -0
  848. mobile_agent/threads/mcp.py +739 -0
  849. mobile_agent/threads/recent.py +34 -0
  850. mobile_agent/threads/register.py +54 -0
  851. mobile_agent/threads/routes.py +188 -0
  852. mobile_agent/threads/service.py +975 -0
  853. mobile_agent/threads/store.py +361 -0
  854. mobile_agent/threads/summary.py +117 -0
  855. mobile_agent/threads/term.py +101 -0
  856. mobile_agent/threads/workflow_cli.py +104 -0
  857. mobile_agent/tls.py +36 -0
  858. mobile_agent/traces.py +271 -0
  859. mobile_agent/tracks.py +235 -0
  860. mobile_agent/transport.py +271 -0
  861. mobile_agent/triggers/__init__.py +1 -0
  862. mobile_agent/triggers/actions.py +204 -0
  863. mobile_agent/triggers/model.py +146 -0
  864. mobile_agent/triggers/register.py +29 -0
  865. mobile_agent/triggers/routes.py +101 -0
  866. mobile_agent/triggers/service.py +387 -0
  867. mobile_agent/triggers/store.py +142 -0
  868. mobile_agent/tui/__init__.py +89 -0
  869. mobile_agent/tui/app.py +582 -0
  870. mobile_agent/tui/chrome.py +321 -0
  871. mobile_agent/tui/commands.py +375 -0
  872. mobile_agent/tui/demo_phone.py +369 -0
  873. mobile_agent/tui/images.py +29 -0
  874. mobile_agent/tui/keys.py +64 -0
  875. mobile_agent/tui/prompt.py +195 -0
  876. mobile_agent/tui/render.py +395 -0
  877. mobile_agent/tui/screens.py +180 -0
  878. mobile_agent/tui/session.py +308 -0
  879. mobile_agent/tui/tasks.py +287 -0
  880. mobile_agent/tui/theme.py +118 -0
  881. mobile_agent/tui/widgets.py +272 -0
  882. mobile_agent/update_check.py +213 -0
  883. mobile_agent/usage.py +184 -0
  884. mobile_agent/verify/__init__.py +0 -0
  885. mobile_agent/verify/__main__.py +41 -0
  886. mobile_agent/verify/a11y.py +189 -0
  887. mobile_agent/verify/actions.py +290 -0
  888. mobile_agent/verify/assertions.py +570 -0
  889. mobile_agent/verify/audit.py +312 -0
  890. mobile_agent/verify/business.py +221 -0
  891. mobile_agent/verify/business_run.py +131 -0
  892. mobile_agent/verify/business_state.py +168 -0
  893. mobile_agent/verify/check_schema.py +398 -0
  894. mobile_agent/verify/checks.py +614 -0
  895. mobile_agent/verify/cli.py +423 -0
  896. mobile_agent/verify/conditions.py +483 -0
  897. mobile_agent/verify/crashes.py +321 -0
  898. mobile_agent/verify/device_audit.py +246 -0
  899. mobile_agent/verify/dialogs.py +287 -0
  900. mobile_agent/verify/evidence.py +705 -0
  901. mobile_agent/verify/fixtures.py +305 -0
  902. mobile_agent/verify/flow.py +213 -0
  903. mobile_agent/verify/l10n.py +141 -0
  904. mobile_agent/verify/netcerts.py +67 -0
  905. mobile_agent/verify/netproxy.py +356 -0
  906. mobile_agent/verify/netrun.py +81 -0
  907. mobile_agent/verify/netshim.m +28 -0
  908. mobile_agent/verify/netshim.py +48 -0
  909. mobile_agent/verify/netstubs.py +132 -0
  910. mobile_agent/verify/overlay.py +174 -0
  911. mobile_agent/verify/perf.py +239 -0
  912. mobile_agent/verify/proof.py +192 -0
  913. mobile_agent/verify/replay_admission.py +48 -0
  914. mobile_agent/verify/report.py +952 -0
  915. mobile_agent/verify/run_cleanup.py +67 -0
  916. mobile_agent/verify/run_links.py +48 -0
  917. mobile_agent/verify/run_perf.py +44 -0
  918. mobile_agent/verify/run_secrets.py +39 -0
  919. mobile_agent/verify/runner.py +976 -0
  920. mobile_agent/verify/runs.py +57 -0
  921. mobile_agent/verify/screen_coverage.py +181 -0
  922. mobile_agent/verify/smart.py +106 -0
  923. mobile_agent/verify/snapshot_run.py +114 -0
  924. mobile_agent/verify/snapshots.py +306 -0
  925. mobile_agent/verify/sound.py +335 -0
  926. mobile_agent/verify/source_observation.py +130 -0
  927. mobile_agent/verify/system_surfaces/__init__.py +7 -0
  928. mobile_agent/verify/system_surfaces/play.py +106 -0
  929. mobile_agent/verify/system_surfaces/spec.py +139 -0
  930. mobile_agent/verify/system_surfaces/stage.py +138 -0
  931. mobile_agent/verify/system_surfaces/surfaces.py +121 -0
  932. mobile_agent/verify/tree.py +223 -0
  933. mobile_agent/verify/verdict.py +377 -0
  934. mobile_agent/verify/wire.py +392 -0
  935. mobile_agent/verify/wirestore.py +176 -0
  936. mobile_agent/waiting/__init__.py +1 -0
  937. mobile_agent/waiting/contacts.py +73 -0
  938. mobile_agent/waiting/drafts.py +136 -0
  939. mobile_agent/waiting/job.py +145 -0
  940. mobile_agent/waiting/messages.py +174 -0
  941. mobile_agent/waiting/rank.py +115 -0
  942. mobile_agent/waiting/register.py +26 -0
  943. mobile_agent/watch.py +502 -0
  944. mobile_agent/wda/__init__.py +2 -0
  945. mobile_agent/wda/frame_clock.py +646 -0
  946. mobile_agent/wda/frame_source.py +325 -0
  947. mobile_agent/wda/frame_store.py +115 -0
  948. mobile_agent/wda/gate.py +163 -0
  949. mobile_agent/wda/gestures.py +291 -0
  950. mobile_agent/wda/guards.py +94 -0
  951. mobile_agent/wda/identity.py +55 -0
  952. mobile_agent/wda/observe.py +884 -0
  953. mobile_agent/wda/patches.py +856 -0
  954. mobile_agent/wda/point_gestures.py +150 -0
  955. mobile_agent/wda/source.py +32 -0
  956. mobile_agent/wda/system_dialogs.py +571 -0
  957. mobile_agent/wda/text.py +694 -0
  958. mobile_agent/wda/timeouts.py +77 -0
  959. mobile_agent/wda/video.py +505 -0
  960. mobile_agent/wda/watchdog.py +243 -0
  961. mobile_agent/wireless/__init__.py +15 -0
  962. mobile_agent/wireless/api.py +354 -0
  963. mobile_agent/wireless/cli.py +287 -0
  964. mobile_agent/wireless/devicectl.py +229 -0
  965. mobile_agent/wireless/exposure.py +33 -0
  966. mobile_agent/wireless/mobster-wifi-pair.c +122 -0
  967. mobile_agent/wireless/monitor.py +332 -0
  968. mobile_agent/wireless/network.py +334 -0
  969. mobile_agent/wireless/onboarding.py +293 -0
  970. mobile_agent/wireless/pair.py +124 -0
  971. mobile_agent/wireless/probe.py +322 -0
  972. mobile_agent/wireless/register.py +8 -0
  973. mobile_agent/wireless/relay.py +179 -0
  974. mobile_agent/wireless/result.py +87 -0
  975. mobile_agent/wireless/store.py +85 -0
  976. mobile_agent/wireless/switch.py +77 -0
  977. mobile_agent/wireless/transport.py +118 -0
  978. mobile_agent/wireless/words.py +81 -0
  979. mobile_agent/wireless/xctestrun.py +83 -0
  980. mobile_agent/workflow_tools.py +59 -0
  981. mobile_agent/workflows/__init__.py +2 -0
  982. mobile_agent/workflows/brief.py +201 -0
  983. mobile_agent/workflows/cron.py +110 -0
  984. mobile_agent/workflows/health.py +257 -0
  985. mobile_agent/workflows/history.py +126 -0
  986. mobile_agent/workflows/records.py +442 -0
  987. mobile_agent/workflows/runs.py +1000 -0
  988. mobile_agent/workflows/scheduler.py +134 -0
  989. mobile_agent/workflows/service.py +973 -0
  990. mobile_agent/workflows/starters.py +285 -0
  991. mobile_agent/workflows/triggers.py +248 -0
  992. mobile_agent/xray/__init__.py +1 -0
  993. mobile_agent/xray/app_cli.py +116 -0
  994. mobile_agent/xray/cli.py +21 -0
  995. mobile_agent/xray/crawl.py +754 -0
  996. mobile_agent/xray/diff.py +171 -0
  997. mobile_agent/xray/diff_cli.py +43 -0
  998. mobile_agent/xray/job.py +184 -0
  999. mobile_agent/xray/paywall.py +188 -0
  1000. mobile_agent/xray/policy.py +305 -0
  1001. mobile_agent/xray/register.py +43 -0
  1002. mobile_agent/xray/report.py +283 -0
  1003. mobile_agent/xray/screens.py +117 -0
  1004. mobster_cli-0.4.3.dist-info/METADATA +93 -0
  1005. mobster_cli-0.4.3.dist-info/RECORD +1009 -0
  1006. mobster_cli-0.4.3.dist-info/WHEEL +5 -0
  1007. mobster_cli-0.4.3.dist-info/entry_points.txt +3 -0
  1008. mobster_cli-0.4.3.dist-info/licenses/LICENSE +21 -0
  1009. mobster_cli-0.4.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,694 @@
1
+ """WebDriverAgent text entry."""
2
+
3
+ import base64
4
+ import re
5
+ import time
6
+ import unicodedata
7
+ from urllib.parse import quote
8
+
9
+ from ..state import truncated_field_value, validate_input_text
10
+ from ..transport import Deadline, NotSent, TransportError
11
+ from .guards import DriverRejection, finite_positive, grounded_target
12
+ from .observe import WDA_FRESH_SECONDS, WDA_READ_MARGIN_SECONDS, element_id
13
+
14
+ # Apps where a message composer sends on Return: a line break typed there as a key would send the text before it.
15
+ CHAT_APPS = frozenset({
16
+ "com.apple.MobileSMS", "net.whatsapp.WhatsApp", "com.facebook.Messenger", "ph.telegra.Telegraph",
17
+ "org.whispersystems.signal", "com.tinyspeck.chatlyio", "com.hammerandchisel.discord", "com.burbn.instagram",
18
+ "com.atebits.Tweetie2", "com.linkedin.LinkedIn", "com.toyopagroup.picaboo", "jp.naver.line", "com.tencent.xin",
19
+ "com.microsoft.skype.teams", "com.google.chat", "com.cardify.tinder", "com.hinge.app", "com.bumble.app"})
20
+ CHAT_FIELD = re.compile(r"\b(?:message|imessage|text message|chat|reply|comment|write a|say something|send)\b|"
21
+ r"(?:\.\.\.|…)$", re.I)
22
+ # Focusing a field raises the keyboard within ~0.5 s on device.
23
+ WDA_KEYBOARD_WAIT_SECONDS = 2.0
24
+ # With a hardware keyboard known, the focusing tap's keyboard is read this long after the tap
25
+ # (not for WDA_KEYBOARD_WAIT_SECONDS / 2 of reads: 1-2 reads of ~1 s on a long simulator list).
26
+ WDA_PARKED_FOCUS_SECONDS = .25
27
+ # Longest text a write takes (a document body); typing keeps validate_input_text's limits.
28
+ WDA_WRITE_MAX_CHARS = 20000
29
+ # The press that opens the edit menu (Paste), and how long its menu is looked for.
30
+ WDA_PASTE_PRESS_MS = 600
31
+ WDA_PASTE_MENU_SECONDS = 3.0
32
+
33
+ # Smart punctuation a keyboard may type for plain ASCII (a typed read-back is compared without it).
34
+ _TYPOGRAPHIC = str.maketrans({"‘": "'", "’": "'", "“": '"', "”": '"', "–": "-", "—": "-", "\u00a0": " "})
35
+
36
+
37
+ class NoKeyboardFocus(TransportError):
38
+ """A focusing tap was made and no keyboard came up: nothing was typed."""
39
+
40
+
41
+ class WheelNotSet(ValueError):
42
+ """A picker wheel didn't reach the value it was given; it is left where it shows (the message says what)."""
43
+
44
+
45
+ # A picker wheel's value: the spellings tried (WDA.set_value) and the steps toward it, read after each.
46
+ WHEEL_SPELLINGS = 6
47
+ WHEEL_STEPS = 40
48
+ _WHEEL_NUMBER = re.compile(r"^\s*(\d+)")
49
+
50
+
51
+ def wheel_number(text):
52
+ """The number a wheel's value or a wanted item starts with ("25 min" -> 25, "05" -> 5), else None."""
53
+ found = _WHEEL_NUMBER.match(text or "")
54
+ return int(found.group(1)) if found else None
55
+
56
+
57
+ def wheel_shows(shown, wanted):
58
+ """Whether a wheel showing ``shown`` is at ``wanted``."""
59
+ if not isinstance(shown, str) or not shown.strip() or not wanted:
60
+ return False
61
+ a, b = wheel_number(shown), wheel_number(wanted)
62
+ if a is not None or b is not None:
63
+ return a == b
64
+ shown_words, wanted_words = shown.casefold().split(), wanted.casefold().split()
65
+ return shown_words == wanted_words or shown_words[:1] == wanted_words[:1]
66
+
67
+
68
+ def wheel_items(wanted, shown):
69
+ """The spellings of ``wanted`` a wheel may list, given the value it shows."""
70
+ out = [wanted]
71
+ bare = re.match(r"(\d+|AM|PM)\b", wanted, re.IGNORECASE)
72
+ if bare:
73
+ out.append(bare.group(1))
74
+ number = wheel_number(wanted)
75
+ if number is not None:
76
+ out += [str(number), f"{number:02d}"]
77
+ unit = " ".join((shown or "").split()[1:]) if wheel_number(shown) is not None else ""
78
+ if unit:
79
+ out += [f"{number} {unit}", f"{number:02d} {unit}"]
80
+ if unit.isalpha(): # "1 hour" on a wheel at "0 hours"
81
+ out.append(f"{number} {unit[:-1] if unit.endswith('s') else unit + 's'}")
82
+ seen, items = set(), []
83
+ for item in out:
84
+ if item and item not in seen:
85
+ seen.add(item)
86
+ items.append(item)
87
+ return items
88
+
89
+
90
+ class FieldNotCleared(TransportError):
91
+ """A field known to hold text wasn't shown to be emptied. Typing now could add to it, so the write stops here."""
92
+
93
+
94
+ class WriteInterrupted(TransportError):
95
+ """A compound write stopped after a request that can change the field. Its last request's answer says nothing about
96
+ the ones before it, so the whole write is unknown, however it ended. Connection recovery doesn't restart it, and a
97
+ further attempt the model asks for goes through the same recheck as any unknown outcome."""
98
+
99
+
100
+ class WDAText:
101
+ """The text part of WDA."""
102
+ _write_sent = False # a request of this write that can change the field may have reached WDA
103
+ # (WDA url, bundle) where a long press offered no Paste: later writes there type at once (_paste).
104
+ _paste_missing = set()
105
+ # WDA URLs whose keyboard stayed parked after a focusing tap (hardware-keyboard mode): no
106
+ # software keyboard will animate in, so waiting for one is skipped. A visible one clears it.
107
+ _parked_keyboards = set()
108
+
109
+ def _sends(self, request, *args, **kwargs):
110
+ """A request that can change a field. Only NotSent proves it never left."""
111
+ try:
112
+ answer = request(*args, **kwargs)
113
+ except NotSent:
114
+ raise
115
+ except Exception:
116
+ self._write_sent = True
117
+ raise
118
+ self._write_sent = True
119
+ return answer
120
+
121
+ def _unique_element(self, role, name, deadline):
122
+ """WDA's id for the one ``role`` element named ``name``, else None, so the caller types another way."""
123
+ name = name.replace("\\", "\\\\").replace("'", "\\'")
124
+ # ``name`` is WDA's identifier-or-label. ``identifier`` is not a predicate attribute: WDA 16.12
125
+ # rejected the query, so until 24 Sep this path never ran and every field was focused by a tap.
126
+ try:
127
+ found = self.call("POST", "/elements", {"using": "predicate string", "value":
128
+ f"type == 'XCUIElementType{role}' AND (name == '{name}' OR label == '{name}')"},
129
+ min(5, deadline.remaining()))
130
+ except TransportError:
131
+ return None
132
+ return element_id(found[0]) if isinstance(found, list) and len(found) == 1 else None
133
+
134
+ def _type_text(self, operation, target, snapshot, text, deadline, multiline=None):
135
+ """TYPE / TYPE_SUBMIT into ``target``."""
136
+ if multiline is None:
137
+ multiline = target.role == "TextView"
138
+ grounded_target(operation, target, snapshot)
139
+ validate_input_text(text, multiline=multiline and operation == "TYPE")
140
+ self._check_current(snapshot, deadline)
141
+ focused = self._clear_stale_query(target, deadline)
142
+ keyboard = getattr(snapshot, "keyboard", "") or ("visible" if "SUBMIT" in target.actions else "")
143
+ if focused:
144
+ # The clear left keyboard focus in the field: no focusing tap, no keyboard wait.
145
+ keyboard = "visible"
146
+ elif keyboard == "visible" and self._focused_elsewhere(target, snapshot, deadline):
147
+ # The keyboard belongs to another field: keystrokes would go there (iOSWorld mem-048,
148
+ # 24 Sep: a whole note body typed into its title). Focus has to move first.
149
+ keyboard = ""
150
+ # A visible keyboard keeps the one-call path; otherwise focus must move, and the element
151
+ # itself moves it reliably (a coordinate tap left mem-041's focus in the title).
152
+ if keyboard != "visible" and self._type_into_element(target, text, deadline,
153
+ submit=operation == "TYPE_SUBMIT", multiline=multiline):
154
+ return None
155
+ if keyboard != "visible":
156
+ # No keyboard yet: focus the field with a tap. Many iOS fields
157
+ # (Safari's address bar, search bars) swap in a different
158
+ # editing field on focus, so an element-bound type fails as
159
+ # "stale element"; keystrokes go to whatever holds focus.
160
+ x, y = target.center
161
+ if multiline:
162
+ # A text view's centre puts the cursor mid-text and keystrokes insert there;
163
+ # its last line puts it at the end, where added text belongs.
164
+ left, top, w, h = target.rect
165
+ x, y = left + w * .9, top + h * .92
166
+ self._sends(self._tap, x * snapshot.width, y * snapshot.height, deadline)
167
+ if not self._await_keyboard(deadline):
168
+ raise NoKeyboardFocus("The field did not take keyboard focus; nothing was typed")
169
+ self._dispatching()
170
+ # Return is sent by the driver, never carried in model-generated text.
171
+ keys = [text, "\n"] if operation == "TYPE_SUBMIT" else [text]
172
+ self._frame_mark(operation)
173
+ self._sends(self.call, "POST", "/wda/keys", {"value": keys}, deadline.remaining())
174
+ self._frame_dispatched()
175
+
176
+ def _clear_stale_query(self, target, deadline):
177
+ """Empty a search field holding an earlier query; only search fields, as others may hold the person's text."""
178
+ return target.role == "SearchField" and self._clear_field(target, deadline)
179
+
180
+ def _focused_elsewhere(self, target, snapshot, deadline):
181
+ """True when WDA reports keyboard focus outside ``target``'s frame; unknown counts as not elsewhere."""
182
+ if sum(e.editable for e in snapshot.elements) < 2:
183
+ return False
184
+ try:
185
+ identifier = element_id(self.call("GET", "/element/active", None, min(2, deadline.remaining())) or {})
186
+ if not identifier:
187
+ return False
188
+ rect = self.call("GET", f"/element/{quote(identifier, safe='')}/rect", None, min(2, deadline.remaining()))
189
+ cx = (rect["x"] + rect["width"] / 2) / snapshot.width
190
+ cy = (rect["y"] + rect["height"] / 2) / snapshot.height
191
+ except (TransportError, TimeoutError, KeyError, TypeError, ZeroDivisionError):
192
+ return False
193
+ x, y, w, h = target.rect
194
+ return not (x - .01 <= cx <= x + w + .01 and y - .01 <= cy <= y + h + .01)
195
+
196
+ def _type_into_element(self, target, text, deadline, submit=False, multiline=False):
197
+ """Type through the element itself when it is unique; False: nothing was typed and the caller uses its own path.
198
+ A focusing tap by coordinates can leave focus in the last field: mem-041 typed a note body into its title."""
199
+ filled = (target.value or "").strip() and (target.value or "").strip() != (target.label or "").strip()
200
+ # A multi-line text field (SwiftUI's vertical TextField) is a text view here: CloudSlides' body
201
+ # took the typed text mid-way (cloudslides-002, 4 of 4 runs, 25 Sep).
202
+ if target.role not in ("TextField", "TextView") or (target.role == "TextView" or multiline) and filled:
203
+ return False
204
+ identifier = target.label and self._unique_element(target.role, target.label, deadline)
205
+ if not identifier:
206
+ return False
207
+ self._dispatching()
208
+ self._frame_mark("TYPE")
209
+ # Once sent, typing may have happened even if WDA never answers. Only the
210
+ # read-only lookup above may fall back to another input route.
211
+ self._sends(self.call, "POST", f"/element/{quote(identifier, safe='')}/value",
212
+ {"value": text + "\n" if submit else text}, deadline.remaining())
213
+ self._frame_dispatched()
214
+ return True
215
+
216
+ def set_value(self, target, value, timeout=10):
217
+ """Select ``value`` ("6", "AM") on the wheel nearest the observed one, read back; WheelNotSet when it can't."""
218
+ deadline = Deadline(timeout)
219
+ found = self.call("POST", "/elements", {"using": "class name", "value": f"XCUIElementType{target.role}"},
220
+ min(5, deadline.remaining()))
221
+ ids = [i for i in (element_id(e) for e in (found if isinstance(found, list) else [])[:12]) if i]
222
+ if not ids:
223
+ raise DriverRejection("unsupported_or_stale_target")
224
+ cx, cy = target.center
225
+ best, best_distance = None, None
226
+ width, height = self._screen_sizes.get(self.url) or (None, None)
227
+ if not width:
228
+ current = self.observe(timeout=min(5, deadline.remaining()))
229
+ width, height = current.width, current.height
230
+ for identifier in ids:
231
+ rect = self.call("GET", f"/element/{quote(identifier, safe='')}/rect", timeout=min(3, deadline.remaining()))
232
+ if not isinstance(rect, dict) or not width:
233
+ continue
234
+ x = (rect.get("x", 0) + rect.get("width", 0) / 2) / width
235
+ y = (rect.get("y", 0) + rect.get("height", 0) / 2) / height
236
+ distance = (x - cx) ** 2 + (y - cy) ** 2
237
+ if best_distance is None or distance < best_distance:
238
+ best, best_distance = identifier, distance
239
+ best = best or (ids[0] if len(ids) == 1 else None)
240
+ if best is None:
241
+ raise DriverRejection("unsupported_or_stale_target")
242
+ self._dispatching()
243
+ wanted = " ".join(str(value).split())
244
+ path = f"/element/{quote(best, safe='')}"
245
+ shown = self._wheel_value(path, deadline)
246
+ if wheel_shows(shown, wanted):
247
+ return
248
+ # WDA ignores a value that is not one of the wheel's items, silently. Items are spelled per picker: an alarm's
249
+ # minutes are "00".."59" ("0" didn't move it: iOSWorld multi-087), a timer's are "7 min" ("7" didn't: verify
250
+ # runs, 12 Oct), the model often names one as the wheel shows it ("6 o'clock", "45 minutes": clock-001). Each
251
+ # spelling is tried, read back after each.
252
+ for attempt in wheel_items(wanted, shown)[:WHEEL_SPELLINGS]:
253
+ self.call("POST", path + "/value", {"value": attempt}, deadline.remaining())
254
+ shown = self._wheel_value(path, deadline)
255
+ if wheel_shows(shown, wanted):
256
+ return
257
+ # No spelling took: the wheel is stepped one item at a time toward the value (WDA's picker-wheel select), and
258
+ # read after every step.
259
+ goal = wheel_number(wanted)
260
+ orders = ["previous", "next"] if goal is None else []
261
+ for _ in range(WHEEL_STEPS):
262
+ if deadline.remaining() <= 1:
263
+ break
264
+ now = wheel_number(shown)
265
+ if goal is not None:
266
+ if now is None or now == goal:
267
+ break
268
+ order = "next" if now < goal else "previous"
269
+ elif orders:
270
+ order = orders.pop(0)
271
+ else:
272
+ break
273
+ self.call("POST", f"/wda/pickerwheel/{quote(best, safe='')}/select", {"order": order, "offset": 0.15},
274
+ deadline.remaining())
275
+ before, shown = shown, self._wheel_value(path, deadline)
276
+ if wheel_shows(shown, wanted):
277
+ return
278
+ if shown == before:
279
+ break # the wheel didn't move: at its end, or the step isn't supported
280
+ # Fails closed: the caller learns the wheel isn't at the value, and a chain stops here (a 25-minute timer
281
+ # started after SET_TEXT "12" returned as if it had worked, verify runs of 12 Oct).
282
+ raise WheelNotSet(f"the wheel shows {str(shown)[:40]!r}, not {wanted[:40]!r}: none of its items matched")
283
+
284
+ def _wheel_value(self, path, deadline):
285
+ try:
286
+ shown = self.call("GET", path + "/attribute/value", timeout=min(3, max(.5, deadline.remaining())))
287
+ except TransportError:
288
+ return None
289
+ return shown if isinstance(shown, str) else None
290
+
291
+ def clear_text(self, target, timeout=10):
292
+ """Empty an editable field (a caller replacing its text: a label, a title). Best effort."""
293
+ self._clear_field(target, Deadline(timeout))
294
+
295
+ def replace_text(self, target, text, timeout=10):
296
+ """Replace a text field's content through one element reference; False: nothing typed, the caller types."""
297
+ validate_input_text(text, multiline=target.role == "TextView")
298
+ query = (target.value or "").strip()
299
+ filled = bool(query) and query != (target.label or "").strip()
300
+ # A search field is typed into by keystrokes, never through the element (Safari swaps in
301
+ # another field on focus): only a filled one, whose clear leaves keyboard focus in it.
302
+ if target.role not in ("TextField", "TextView", "SearchField") or not target.label or (
303
+ target.role == "SearchField" and not filled):
304
+ return False
305
+ deadline = Deadline(timeout)
306
+ identifier = self._unique_element(target.role, target.label, deadline)
307
+ if not identifier:
308
+ return False
309
+ path = f"/element/{quote(identifier, safe='')}"
310
+ self._dispatching()
311
+ if filled:
312
+ self.call("POST", path + "/clear", {}, min(5, deadline.remaining()))
313
+ self._frame_mark("TYPE")
314
+ if target.role == "SearchField":
315
+ self._sends(self.call, "POST", "/wda/keys", {"value": [text]}, deadline.remaining())
316
+ else:
317
+ self.call("POST", path + "/value", {"value": text}, deadline.remaining())
318
+ self._frame_dispatched()
319
+ return True
320
+
321
+ def _clear_field(self, target, deadline):
322
+ query = (target.value or "").strip()
323
+ if (target.role not in {"SearchField", "TextField", "TextView"} or not query
324
+ or query == (target.label or "").strip()):
325
+ return False
326
+ def literal(text):
327
+ return text.replace("\\", "\\\\").replace("'", "\\'")
328
+ name = (target.label or "").strip()
329
+ kind = f"type == 'XCUIElementType{target.role}'"
330
+ # WDA re-finds an element through the query that found it. Found by its value, the field
331
+ # was gone once emptied: /clear emptied it, then failed after ~2.7 s ("not present in the
332
+ # current view anymore"; 83 s of mem-015's 276 s, 24 Sep). By name, or as the only field of
333
+ # its kind, the same clear takes ~0.4 s; the value finds it only when neither is unique. A long
334
+ # value is read truncated ("head … tail", state._wda_text): its head finds it (an exact match
335
+ # found nothing, and SET_TEXT appended to a note body: iOSWorld mem-004, 24 Sep).
336
+ by_value = (f"{kind} AND value == '{literal(query)}'" if " … " not in query and len(query) < 500
337
+ else f"{kind} AND value BEGINSWITH '{literal(query.split(' … ', 1)[0][:150])}'")
338
+ queries = ([f"{kind} AND (name == '{literal(name)}' OR label == '{literal(name)}')"] if name else []) + [
339
+ kind, by_value]
340
+ for predicate in queries:
341
+ try:
342
+ found = self.call("POST", "/elements", {"using": "predicate string", "value": predicate},
343
+ min(5, deadline.remaining()))
344
+ except TransportError:
345
+ continue
346
+ ids = [i for i in (element_id(e) for e in (found if isinstance(found, list) else [])) if i]
347
+ if len(ids) == 1:
348
+ self._dispatching()
349
+ self._sends(self.call, "POST", f"/element/{quote(ids[0], safe='')}/clear", {},
350
+ min(5, deadline.remaining()))
351
+ # A unique field was cleared and focused by XCTest.
352
+ return True
353
+ if ids and predicate == by_value:
354
+ # Several fields hold this text: clearing the first could empty the wrong one.
355
+ raise DriverRejection("unsupported_or_stale_target")
356
+ return False
357
+
358
+ def _await_keyboard(self, deadline, seconds=None):
359
+ """True once a software keyboard is up, or, when none appears, a hardware one is attached.
360
+
361
+ A parked keyboard (off screen: simulators, a paired keyboard) still takes keystrokes
362
+ in the focused field. Measured on iOSWorld's simulators (24 Sep): 3 of 4 ran in
363
+ hardware-keyboard mode and every TYPE failed "did not take keyboard focus" although
364
+ /wda/keys typed fine. A visible keyboard is still preferred while it may be animating in.
365
+ """
366
+ tapped = time.monotonic()
367
+ end = tapped + (WDA_KEYBOARD_WAIT_SECONDS if seconds is None else seconds)
368
+ parked = False
369
+ hardware = self.url in self._parked_keyboards
370
+ if hardware:
371
+ # No software keyboard will come: one read, once focus has had time to move.
372
+ time.sleep(max(0.0, min(tapped + WDA_PARKED_FOCUS_SECONDS - time.monotonic(),
373
+ deadline.end - time.monotonic() - WDA_READ_MARGIN_SECONDS)))
374
+ first = True
375
+ # At least one read: a slow host's focus sleep can outlast the window (CI read nothing, 25 Sep).
376
+ while (first or time.monotonic() < end) and deadline.end - time.monotonic() > WDA_READ_MARGIN_SECONDS:
377
+ first = False
378
+ current = self.observe(timeout=deadline.remaining())
379
+ keyboard = getattr(current, "keyboard", "") or (
380
+ "visible" if any("SUBMIT" in e.actions for e in current.elements) else "")
381
+ if keyboard == "visible":
382
+ self._parked_keyboards.discard(self.url)
383
+ return True
384
+ parked = parked or keyboard == "parked"
385
+ if parked and (hardware or time.monotonic() > end - WDA_KEYBOARD_WAIT_SECONDS / 2):
386
+ self._parked_keyboards.add(self.url)
387
+ return True
388
+ if parked:
389
+ self._parked_keyboards.add(self.url)
390
+ return parked
391
+
392
+ def write_text(self, target, snapshot, text, append=False, submit=False, timeout=30):
393
+ """Write ``text`` into ``target`` and prove it by reading the whole field back.
394
+ An append is typed at the end of the field's text and never empties it; a replace whose read-back differs is
395
+ repaired once (emptied, written again). An append's read-back is reported as it is.
396
+ Failing before anything that can change the field keeps its own error, so a caller may resend. Failing after is
397
+ WriteInterrupted, whatever the last request said."""
398
+ self._write_sent = False
399
+ try:
400
+ return self._write_whole(target, snapshot, text, append, submit, timeout)
401
+ except Exception as error:
402
+ if not self._write_sent:
403
+ raise
404
+ raise WriteInterrupted("The write stopped partway; request outcome unknown; not retried") from error
405
+
406
+ def _write_whole(self, target, snapshot, text, append, submit, timeout):
407
+ deadline = Deadline(timeout)
408
+ grounded_target("TYPE", target, snapshot)
409
+ rank = self._write_rank = self._rank(snapshot, target)
410
+ if (not isinstance(text, str) or len(text) > WDA_WRITE_MAX_CHARS or append and not text.strip()
411
+ or any(unicodedata.category(c) in {"Cc", "Cs", "Zl", "Zp"} and c not in "\n\t" for c in text)):
412
+ raise ValueError("TYPE requires bounded text without control characters")
413
+ text = text.replace("\t", " ")
414
+ shown = target.text if target.placeholder else (target.value or "")
415
+ multiline = target.role == "TextView" or "\n" in shown
416
+ # A chat composer sends on Return: its line breaks are pasted, or become spaces, never typed as keys.
417
+ chat = (getattr(snapshot, "bundle_id", "") in CHAT_APPS
418
+ or bool(CHAT_FIELD.search(f"{target.label or ''} {target.placeholder or ''}")))
419
+ breaks = "\n" in text and (not multiline or chat)
420
+ old = self._field_text(target, snapshot, deadline) if append else ""
421
+ # An empty field reads as its placeholder, which not every field names (SwiftUI's vertical
422
+ # TextField does not): an earlier text is known only when it cannot be a placeholder.
423
+ known = old is not None and (not old or target.role == "TextView" or bool(target.placeholder)
424
+ or "\n" in old or len(old) >= 60)
425
+ # An append is typed at the end of what the field holds, never pasted: a paste writes the whole field, so the
426
+ # field is emptied first, and a paste or retype that then fails leaves it empty (iOSWorld multi-087, 5 Nov 2026:
427
+ # a packing list in Notes was emptied and saved so).
428
+ paste = bool(text) and not append and (len(text) >= self.paste_min_chars or breaks) and old is not None
429
+ receipt = {"method": "clear" if not text else "paste" if paste else "type", "repaired": False,
430
+ "flattened": False}
431
+ current, field = snapshot, target
432
+ if not append:
433
+ if self._emptied(field, current, deadline):
434
+ current, field = self._reread(field, deadline)
435
+ expected = (old or "") + text if append else text
436
+ if paste and not self._paste(field, current, expected, deadline):
437
+ receipt["method"] = "type"
438
+ if receipt["method"] == "type":
439
+ typed = text
440
+ if breaks:
441
+ # Never Return in a field not known to take line breaks: it may submit.
442
+ typed, receipt["flattened"] = " ".join(typed.split("\n")), True
443
+ expected = " ".join(expected.split("\n"))
444
+ self._type_chunks(field, current, typed, deadline, multiline)
445
+ value, current = self._read_back(target, deadline, rank)
446
+ matches = self._fits(value, expected, text, append and not known, breaks, field)
447
+ if not matches and value is not None and breaks and self._same_text(value, " ".join(expected.split("\n"))):
448
+ receipt["flattened"], matches = True, True # a one-line field: its line breaks became spaces
449
+ if not text and not matches and value is not None and value != shown:
450
+ # Emptied, yet it reads as something: emptied again, what stays is its placeholder.
451
+ field = self._same_field(current, target, rank) or field
452
+ self._emptied(field, current, deadline)
453
+ again, current = self._read_back(target, deadline, rank)
454
+ if again == value:
455
+ receipt["placeholder"], value, matches = value, "", True
456
+ elif not matches and value is not None and not append:
457
+ # A replace is repaired once: emptied and written again. An append never is: its field holds the person's
458
+ # text, and emptying it to rewrite the whole lost a note when the rewrite didn't land (multi-087: Notes'
459
+ # list formatting changed the added lines, the read-back differed, and the note was left empty). Its
460
+ # read-back is reported as it is.
461
+ receipt["repaired"] = True
462
+ field = self._same_field(current, target, rank) or field
463
+ if self._emptied(field, current, deadline):
464
+ current, field = self._reread(field, deadline)
465
+ if not self._paste(field, current, expected, deadline):
466
+ self._type_chunks(field, None, " ".join(expected.split("\n")) if breaks else expected,
467
+ deadline, multiline)
468
+ value, current = self._read_back(target, deadline, rank)
469
+ matches = self._fits(value, expected, text, False, breaks, field)
470
+ # Return only on a field proven to hold what was written: a garbled search runs, a garbled message sends
471
+ # (9 Oct: Safari searched "Grace Hopperen.m.wikipedia.org/wiki/IOS 26"). Fails closed.
472
+ receipt["submitted"] = bool(submit and matches)
473
+ if submit and matches:
474
+ self._dispatching()
475
+ self._frame_mark("SUBMIT")
476
+ self._sends(self.call, "POST", "/wda/keys", {"value": ["\n"]}, deadline.remaining())
477
+ self._frame_dispatched()
478
+ receipt.update(value=value, expected=expected, matches=bool(matches),
479
+ chars=None if value is None else len(value))
480
+ return receipt
481
+
482
+ def _fits(self, value, expected, text, unknown_start, breaks, field):
483
+ """The field's text read back is what was written."""
484
+ if self._same_text(value, expected, field=field):
485
+ return True
486
+ if unknown_start and value is not None:
487
+ return self._same_text(value, text, suffix=True) or breaks and self._same_text(
488
+ value, " ".join(text.split("\n")), suffix=True)
489
+ return False
490
+
491
+ @staticmethod
492
+ def _same_text(value, expected, suffix=False, field=None):
493
+ """``value`` (a field read back) is ``expected``."""
494
+ if value is None:
495
+ return False
496
+ if not expected and field is not None and field.placeholder and value == field.placeholder:
497
+ return True
498
+
499
+ def plain(text):
500
+ return text.translate(_TYPOGRAPHIC).replace("\r\n", "\n")
501
+ return (value == expected or plain(value) == plain(expected)
502
+ or suffix and plain(value).endswith(plain(expected)))
503
+
504
+ @staticmethod
505
+ def _rank(snapshot, target):
506
+ """(fields of ``target``'s kind on ``snapshot``, its place among them)."""
507
+ fields = [e for e in snapshot.elements if e.editable and e.role == target.role]
508
+ return len(fields), next((i for i, e in enumerate(fields) if e == target), None)
509
+
510
+ def _same_field(self, snapshot, target, rank=None):
511
+ """``target`` on a later ``snapshot``."""
512
+ fields = [e for e in snapshot.elements if e.editable and e.role == target.role]
513
+ same = [e for e in fields if e.locator == target.locator]
514
+ if len(same) == 1:
515
+ return same[0]
516
+ if target.label:
517
+ named = [e for e in fields if e.label == target.label]
518
+ if len(named) == 1:
519
+ return named[0]
520
+ if rank is not None and rank[1] is not None and len(fields) == rank[0]:
521
+ return fields[rank[1]]
522
+
523
+ def distance(e):
524
+ return abs(e.rect[0] - target.rect[0]) + abs(e.rect[1] - target.rect[1])
525
+ near = min(fields, key=distance, default=None)
526
+ return near if near is not None and distance(near) < .08 else None
527
+
528
+ def _field_text(self, target, snapshot, deadline):
529
+ """The field's whole text now, or None when it cannot be read."""
530
+ if target.placeholder and target.value == target.placeholder:
531
+ return ""
532
+ if not truncated_field_value(target.value):
533
+ return target.value or ""
534
+ return self._active_text(target, snapshot, deadline, focus=True)
535
+
536
+ def _active_id(self, target, snapshot, deadline, focus=False):
537
+ """WDA's id for the field holding keyboard focus when it is ``target`` (their frames agree), else None."""
538
+ for attempt in range(2 if focus else 1):
539
+ try:
540
+ identifier = element_id(self.call("GET", "/element/active", None, min(3, deadline.remaining())) or {})
541
+ if identifier:
542
+ rect = self.call("GET", f"/element/{quote(identifier, safe='')}/rect", None,
543
+ min(3, deadline.remaining()))
544
+ if self._frame_agrees(rect, target, snapshot):
545
+ return identifier
546
+ except (TransportError, KeyError, TypeError):
547
+ pass
548
+ if attempt == 0 and focus:
549
+ x, y = target.center
550
+ self._sends(self._tap, x * snapshot.width, y * snapshot.height, deadline)
551
+ time.sleep(WDA_PARKED_FOCUS_SECONDS)
552
+ return None
553
+
554
+ def _active_text(self, target, snapshot, deadline, focus=False):
555
+ identifier = self._active_id(target, snapshot, deadline, focus)
556
+ if identifier is None:
557
+ return None
558
+ path = f"/element/{quote(identifier, safe='')}/attribute/"
559
+ try:
560
+ value = self.call("GET", path + "value", None, min(3, deadline.remaining()))
561
+ if value and target.role != "TextView":
562
+ placeholder = self.call("GET", path + "placeholderValue", None, min(3, deadline.remaining()))
563
+ if placeholder and value == placeholder:
564
+ return ""
565
+ except (TransportError, TypeError):
566
+ return None
567
+ return value if isinstance(value, str) else ""
568
+
569
+ @staticmethod
570
+ def _frame_agrees(rect, target, snapshot):
571
+ """A WDA rect (points) is ``target``'s frame."""
572
+ if not isinstance(rect, dict):
573
+ return False
574
+ try:
575
+ ax, ay, aw, ah = (float(rect[k]) / d for k, d in (("x", snapshot.width), ("y", snapshot.height),
576
+ ("width", snapshot.width), ("height", snapshot.height)))
577
+ except (KeyError, TypeError, ValueError):
578
+ return False
579
+ tx, ty, tw, th = target.rect
580
+ slack = .01
581
+ return (ax - slack <= tx + tw / 2 <= ax + aw + slack and ay - slack <= ty + th / 2 <= ay + ah + slack
582
+ or tx - slack <= ax + aw / 2 <= tx + tw + slack and ty - slack <= ay + ah / 2 <= ty + th + slack)
583
+
584
+ def _emptied(self, target, snapshot, deadline):
585
+ """True once ``target`` was emptied, False when it already was (and is left alone)."""
586
+ text = target.text if target.placeholder else (target.value or "")
587
+ if not text.strip() or text.strip() == (target.label or "").strip() and not target.placeholder:
588
+ return False
589
+ self._dispatching()
590
+ # A named field is cleared by its name; an unnamed one (CloudSlides' slide body, one of several
591
+ # text fields) through the field holding focus: asking WDA for every field of its kind took 1.8 s.
592
+ if not (target.label and self._clear_field(target, deadline)):
593
+ identifier = self._active_id(target, snapshot, deadline, focus=True)
594
+ if identifier is None:
595
+ raise FieldNotCleared("The field's text couldn't be cleared; no more was written")
596
+ # A lost answer may follow a sent clear: the error stops the write rather than typing over it.
597
+ self._sends(self.call, "POST", f"/element/{quote(identifier, safe='')}/clear", {},
598
+ min(8, deadline.remaining()))
599
+ return True
600
+
601
+ def _paste(self, target, snapshot, text, deadline):
602
+ """Paste ``text`` through the empty field's edit menu (pressed twice at most); False when no Paste showed."""
603
+ where = (self.url, getattr(snapshot, "bundle_id", None))
604
+ if where in self._paste_missing:
605
+ # This app offered no Paste before: the second miss cost another ~12 s for nothing (the frontier trace
606
+ # review measured 15 s writes on the phone, 9 Oct). Typing goes on as the fallback always did.
607
+ return False
608
+ try:
609
+ self.call("POST", "/wda/setPasteboard", {"content": base64.b64encode(text.encode()).decode(),
610
+ "contentType": "plaintext"}, min(5, deadline.remaining()))
611
+ except TransportError:
612
+ return False
613
+ # An Allow Paste that follows is Mobster's own paste of this text (system_dialogs answers it as such).
614
+ from .system_dialogs import note_paste
615
+ note_paste(self)
616
+ left, top, width, height = target.rect
617
+ x = left + width / 2
618
+ y = top + min(height / 2, 22 / snapshot.height) # the first line: a long press on text moves the caret
619
+ self._dispatching()
620
+ self._frame_mark("TYPE")
621
+ try:
622
+ for attempt in range(2):
623
+ if attempt:
624
+ self._sends(self._tap, x * snapshot.width, y * snapshot.height, deadline)
625
+ time.sleep(WDA_PARKED_FOCUS_SECONDS)
626
+ self._sends(self._pointer, [
627
+ {"type": "pointerMove", "duration": 0, "x": round(x * snapshot.width),
628
+ "y": round(y * snapshot.height)},
629
+ {"type": "pointerDown"}, {"type": "pause", "duration": WDA_PASTE_PRESS_MS},
630
+ {"type": "pointerUp"}], deadline)
631
+ end = time.monotonic() + WDA_PASTE_MENU_SECONDS
632
+ while time.monotonic() < end and deadline.end - time.monotonic() > WDA_READ_MARGIN_SECONDS:
633
+ # One query, not a screen read: a read may also ask WDA which layers show (up to 3 s
634
+ # a read in CloudSlides' editor, 26 Sep).
635
+ try:
636
+ found = self.call("POST", "/elements", {"using": "predicate string", "value":
637
+ "type IN {'XCUIElementTypeMenuItem', 'XCUIElementTypeButton'} "
638
+ "AND label == 'Paste'"}, min(3, deadline.remaining()))
639
+ identifier = element_id(found[0]) if isinstance(found, list) and found else None
640
+ rect = identifier and self.call("GET", f"/element/{quote(identifier, safe='')}/rect", None,
641
+ min(3, deadline.remaining()))
642
+ except TransportError:
643
+ identifier = rect = None
644
+ if isinstance(rect, dict) and all(finite_positive(rect.get(k)) or rect.get(k) == 0
645
+ for k in ("x", "y", "width", "height")):
646
+ self._sends(self._tap, rect["x"] + rect["width"] / 2, rect["y"] + rect["height"] / 2, deadline)
647
+ return True
648
+ time.sleep(.1)
649
+ self._paste_missing.add(where)
650
+ return False
651
+ finally:
652
+ self._frame_dispatched()
653
+
654
+ def _reread(self, target, deadline):
655
+ """(a new read, ``target`` on it)."""
656
+ fresh = self.observe(timeout=min(5, deadline.remaining()))
657
+ return fresh, self._same_field(fresh, target, self._write_rank) or target
658
+
659
+ def _type_chunks(self, target, snapshot, text, deadline, multiline):
660
+ """Type ``text`` (a known multi-line field takes its line breaks), in chunks typing accepts."""
661
+ limit = 4000 if multiline else 1000
662
+ chunks = [text[i:i + limit] for i in range(0, len(text), limit)] or [""]
663
+ fresh = snapshot
664
+ if fresh is None or time.monotonic() - fresh.captured_at > WDA_FRESH_SECONDS:
665
+ fresh, target = self._reread(target, deadline)
666
+ first = chunks[0]
667
+ if first.strip() and self._active_id(target, fresh, deadline) is not None:
668
+ # The field holds focus (it was just emptied through it): keystrokes, no focusing tap. A tap on
669
+ # an emptied CloudSlides body left it without focus every second write (F2, 26 Sep).
670
+ self._dispatching()
671
+ self._frame_mark("TYPE")
672
+ self._sends(self.call, "POST", "/wda/keys", {"value": [first]}, deadline.remaining())
673
+ self._frame_dispatched()
674
+ elif first.strip():
675
+ try:
676
+ self._type_text("TYPE", target, fresh, first, deadline, multiline=multiline)
677
+ except NoKeyboardFocus:
678
+ # The focusing tap was made but no keyboard was seen (none shows with a hardware keyboard
679
+ # on some screens): keystrokes go where focus is, so only when the field holds it.
680
+ if self._active_id(target, fresh, deadline) is None:
681
+ raise
682
+ self._frame_mark("TYPE")
683
+ self._sends(self.call, "POST", "/wda/keys", {"value": [first]}, deadline.remaining())
684
+ self._frame_dispatched()
685
+ for chunk in chunks[1:]:
686
+ self._sends(self.call, "POST", "/wda/keys", {"value": [chunk]}, deadline.remaining())
687
+
688
+ def _read_back(self, target, deadline, rank=None):
689
+ """(the field's whole text, the read)."""
690
+ fresh = self.observe(timeout=min(5, deadline.remaining()))
691
+ field = self._same_field(fresh, target, rank)
692
+ if field is not None and not truncated_field_value(field.value):
693
+ return (field.text if field.placeholder else field.value or ""), fresh
694
+ return self._active_text(field or target, fresh, deadline), fresh