android-midscene-automation 0.2.3 → 1.1.0

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 (525) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/README.md +63 -1
  3. package/artemis-main/.dockerignore +15 -0
  4. package/artemis-main/.env.example +34 -0
  5. package/artemis-main/CONTRIBUTING.md +35 -0
  6. package/artemis-main/Dockerfile +68 -0
  7. package/artemis-main/LICENSE +202 -0
  8. package/artemis-main/Makefile +115 -0
  9. package/artemis-main/README.md +322 -0
  10. package/artemis-main/README_CN.md +320 -0
  11. package/artemis-main/apps/__init__.py +15 -0
  12. package/artemis-main/apps/admin_console/__init__.py +19 -0
  13. package/artemis-main/apps/admin_console/core/__init__.py +13 -0
  14. package/artemis-main/apps/admin_console/core/config.py +43 -0
  15. package/artemis-main/apps/admin_console/core/security.py +178 -0
  16. package/artemis-main/apps/admin_console/core/state.py +243 -0
  17. package/artemis-main/apps/admin_console/database/__init__.py +13 -0
  18. package/artemis-main/apps/admin_console/database/connection.py +60 -0
  19. package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
  20. package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
  21. package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
  22. package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
  23. package/artemis-main/apps/admin_console/index.html +3787 -0
  24. package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
  25. package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
  26. package/artemis-main/apps/admin_console/routers/media.py +198 -0
  27. package/artemis-main/apps/admin_console/routers/replay.py +91 -0
  28. package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
  29. package/artemis-main/apps/admin_console/routers/steps.py +109 -0
  30. package/artemis-main/apps/admin_console/routers/stream.py +56 -0
  31. package/artemis-main/apps/admin_console/routers/system.py +761 -0
  32. package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
  33. package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
  34. package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
  35. package/artemis-main/apps/admin_console/server.py +508 -0
  36. package/artemis-main/apps/admin_console/services/__init__.py +13 -0
  37. package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
  38. package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
  39. package/artemis-main/apps/admin_console/services/media_service.py +623 -0
  40. package/artemis-main/apps/admin_console/services/model_service.py +148 -0
  41. package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
  42. package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
  43. package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
  44. package/artemis-main/apps/showcase_ui/README.md +31 -0
  45. package/artemis-main/apps/showcase_ui/angular.json +132 -0
  46. package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
  47. package/artemis-main/apps/showcase_ui/package.json +39 -0
  48. package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
  49. package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
  50. package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
  51. package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
  52. package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
  53. package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
  54. package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
  55. package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
  56. package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
  57. package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
  58. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
  59. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
  60. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
  61. package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
  62. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
  63. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
  64. package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
  65. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
  66. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
  67. package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
  68. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
  69. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
  70. package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
  71. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
  72. package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
  73. package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
  74. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
  75. package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
  76. package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
  77. package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
  78. package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
  79. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
  80. package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
  81. package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
  82. package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
  83. package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
  84. package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
  85. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
  86. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
  87. package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
  88. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
  89. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
  90. package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
  91. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
  92. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
  93. package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
  94. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
  95. package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
  96. package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
  97. package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
  98. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
  99. package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
  100. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
  101. package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
  102. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
  103. package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
  104. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
  105. package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
  106. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
  107. package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
  108. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
  109. package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
  110. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
  111. package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
  112. package/artemis-main/apps/showcase_ui/src/index.html +37 -0
  113. package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
  114. package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
  115. package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
  116. package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
  117. package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
  118. package/artemis-main/artemis/__init__.py +44 -0
  119. package/artemis-main/artemis/__main__.py +18 -0
  120. package/artemis-main/artemis/_legacy_imports.py +94 -0
  121. package/artemis-main/artemis/_version.py +30 -0
  122. package/artemis-main/artemis/agents/__init__.py +17 -0
  123. package/artemis-main/artemis/agents/checker/checker.json +9 -0
  124. package/artemis-main/artemis/agents/checker/checker.py +859 -0
  125. package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
  126. package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
  127. package/artemis-main/artemis/agents/explorer/constants.py +52 -0
  128. package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
  129. package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
  130. package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
  131. package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
  132. package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
  133. package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
  134. package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
  135. package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
  136. package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
  137. package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
  138. package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
  139. package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
  140. package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
  141. package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
  142. package/artemis-main/artemis/agents/flash/runner.py +1167 -0
  143. package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
  144. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
  145. package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
  146. package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
  147. package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
  148. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
  149. package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
  150. package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
  151. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
  152. package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
  153. package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
  154. package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
  155. package/artemis-main/artemis/agents/operator/operator.json +3 -0
  156. package/artemis-main/artemis/agents/operator/operator.py +1394 -0
  157. package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
  158. package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
  159. package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
  160. package/artemis-main/artemis/agents/planner/planner.json +32 -0
  161. package/artemis-main/artemis/agents/planner/planner.py +436 -0
  162. package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
  163. package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
  164. package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
  165. package/artemis-main/artemis/agents/validator/categories.py +33 -0
  166. package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
  167. package/artemis-main/artemis/agents/validator/incidents.py +120 -0
  168. package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
  169. package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
  170. package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
  171. package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
  172. package/artemis-main/artemis/agents/validator/validator.py +188 -0
  173. package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
  174. package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
  175. package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
  176. package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
  177. package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
  178. package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
  179. package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
  180. package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
  181. package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
  182. package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
  183. package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
  184. package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
  185. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
  186. package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
  187. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
  188. package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
  189. package/artemis-main/artemis/clients/accessibility_client.py +438 -0
  190. package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
  191. package/artemis-main/artemis/config/__init__.py +309 -0
  192. package/artemis-main/artemis/config/agent.py +1098 -0
  193. package/artemis-main/artemis/config/constants.py +151 -0
  194. package/artemis-main/artemis/config/core/__init__.py +24 -0
  195. package/artemis-main/artemis/config/core/engine.py +118 -0
  196. package/artemis-main/artemis/config/core/interpolator.py +78 -0
  197. package/artemis-main/artemis/config/llm.py +267 -0
  198. package/artemis-main/artemis/config/paths.py +244 -0
  199. package/artemis-main/artemis/config/runtime.py +197 -0
  200. package/artemis-main/artemis/config/settings.py +315 -0
  201. package/artemis-main/artemis/constants.py +47 -0
  202. package/artemis-main/artemis/context.py +289 -0
  203. package/artemis-main/artemis/controllers/__init__.py +41 -0
  204. package/artemis-main/artemis/controllers/controller_factory.py +41 -0
  205. package/artemis-main/artemis/controllers/device_controller.py +29 -0
  206. package/artemis-main/artemis/controllers/types.py +37 -0
  207. package/artemis-main/artemis/controllers/unified_controller.py +649 -0
  208. package/artemis-main/artemis/core/__init__.py +21 -0
  209. package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
  210. package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
  211. package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
  212. package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
  213. package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
  214. package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
  215. package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
  216. package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
  217. package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
  218. package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
  219. package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
  220. package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
  221. package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
  222. package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
  223. package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
  224. package/artemis-main/artemis/core/tool_declaration.py +43 -0
  225. package/artemis-main/artemis/core/tool_failure.py +56 -0
  226. package/artemis-main/artemis/data_engine/context_vars.py +24 -0
  227. package/artemis-main/artemis/data_engine/engine.py +1615 -0
  228. package/artemis-main/artemis/data_engine/history_reader.py +142 -0
  229. package/artemis-main/artemis/data_engine/models.py +131 -0
  230. package/artemis-main/artemis/data_engine/storage.py +1245 -0
  231. package/artemis-main/artemis/data_engine/trace.py +727 -0
  232. package/artemis-main/artemis/drivers/__init__.py +48 -0
  233. package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
  234. package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
  235. package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
  236. package/artemis-main/artemis/drivers/android/recorder.py +57 -0
  237. package/artemis-main/artemis/drivers/base.py +290 -0
  238. package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
  239. package/artemis-main/artemis/drivers/factory.py +80 -0
  240. package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
  241. package/artemis-main/artemis/drivers/types.py +67 -0
  242. package/artemis-main/artemis/graph/checkpoints.py +1243 -0
  243. package/artemis-main/artemis/graph/graph.py +1055 -0
  244. package/artemis-main/artemis/graph/perception.py +234 -0
  245. package/artemis-main/artemis/graph/state.py +151 -0
  246. package/artemis-main/artemis/graph/visibility.py +238 -0
  247. package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
  248. package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
  249. package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
  250. package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
  251. package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
  252. package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
  253. package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
  254. package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
  255. package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
  256. package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
  257. package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
  258. package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
  259. package/artemis-main/artemis/interfaces/cli/main.py +109 -0
  260. package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
  261. package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
  262. package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
  263. package/artemis-main/artemis/llm/__init__.py +25 -0
  264. package/artemis-main/artemis/llm/fake_model.py +179 -0
  265. package/artemis-main/artemis/llm/google/__init__.py +57 -0
  266. package/artemis-main/artemis/llm/google/provider.py +185 -0
  267. package/artemis-main/artemis/llm/google/usage.py +148 -0
  268. package/artemis-main/artemis/llm/parser.py +32 -0
  269. package/artemis-main/artemis/llm/reliability.py +267 -0
  270. package/artemis-main/artemis/llm/router.py +389 -0
  271. package/artemis-main/artemis/llm/schemas.py +36 -0
  272. package/artemis-main/artemis/llm/structured.py +196 -0
  273. package/artemis-main/artemis/main.py +58 -0
  274. package/artemis-main/artemis/mcp/__init__.py +20 -0
  275. package/artemis-main/artemis/mcp/action_executor.py +760 -0
  276. package/artemis-main/artemis/mcp/action_manifest.py +320 -0
  277. package/artemis-main/artemis/mcp/action_names.py +172 -0
  278. package/artemis-main/artemis/mcp/action_server.py +202 -0
  279. package/artemis-main/artemis/mcp/action_session.py +245 -0
  280. package/artemis-main/artemis/mcp/action_specs.py +868 -0
  281. package/artemis-main/artemis/mcp/action_types.py +86 -0
  282. package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
  283. package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
  284. package/artemis-main/artemis/mcp/actuators/base.py +113 -0
  285. package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
  286. package/artemis-main/artemis/mcp/adb_server.py +490 -0
  287. package/artemis-main/artemis/mcp/observation.py +133 -0
  288. package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
  289. package/artemis-main/artemis/memory/__init__.py +90 -0
  290. package/artemis-main/artemis/memory/chunking.py +1843 -0
  291. package/artemis-main/artemis/memory/context_policy.py +226 -0
  292. package/artemis-main/artemis/memory/step_capsule.md +73 -0
  293. package/artemis-main/artemis/memory/step_memory.py +281 -0
  294. package/artemis-main/artemis/memory/transcript.py +798 -0
  295. package/artemis-main/artemis/platform/__init__.py +61 -0
  296. package/artemis-main/artemis/platform/base.py +104 -0
  297. package/artemis-main/artemis/platform/darwin.py +184 -0
  298. package/artemis-main/artemis/platform/linux.py +207 -0
  299. package/artemis-main/artemis/platform/windows.py +193 -0
  300. package/artemis-main/artemis/resources/__init__.py +46 -0
  301. package/artemis-main/artemis/runtime/__init__.py +108 -0
  302. package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
  303. package/artemis-main/artemis/runtime/awake_lease.py +283 -0
  304. package/artemis-main/artemis/runtime/awake_service.py +343 -0
  305. package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
  306. package/artemis-main/artemis/runtime/daemon_client.py +422 -0
  307. package/artemis-main/artemis/runtime/device_lock.py +1013 -0
  308. package/artemis-main/artemis/runtime/device_pool.py +478 -0
  309. package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
  310. package/artemis-main/artemis/runtime/process_probe.py +98 -0
  311. package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
  312. package/artemis-main/artemis/runtime/supervisor.py +121 -0
  313. package/artemis-main/artemis/runtime/trace_store.py +375 -0
  314. package/artemis-main/artemis/sdk/__init__.py +26 -0
  315. package/artemis-main/artemis/sdk/agent.py +485 -0
  316. package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
  317. package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
  318. package/artemis-main/artemis/sdk/constants.py +15 -0
  319. package/artemis-main/artemis/sdk/run_outcome.py +58 -0
  320. package/artemis-main/artemis/sdk/types/__init__.py +61 -0
  321. package/artemis-main/artemis/sdk/types/agent.py +89 -0
  322. package/artemis-main/artemis/sdk/utils.py +28 -0
  323. package/artemis-main/artemis/services/llm.py +1040 -0
  324. package/artemis-main/artemis/services/token_meter.py +267 -0
  325. package/artemis-main/artemis/telemetry/__init__.py +35 -0
  326. package/artemis-main/artemis/telemetry/models.py +64 -0
  327. package/artemis-main/artemis/telemetry/replay.py +34 -0
  328. package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
  329. package/artemis-main/artemis/telemetry/tracer.py +85 -0
  330. package/artemis-main/artemis/toolchain/__init__.py +52 -0
  331. package/artemis-main/artemis/toolchain/descriptors.py +92 -0
  332. package/artemis-main/artemis/toolchain/resolver.py +185 -0
  333. package/artemis-main/artemis/tools/base.py +232 -0
  334. package/artemis-main/artemis/tools/command_tool.py +1240 -0
  335. package/artemis-main/artemis/tools/committee_prompts.json +5 -0
  336. package/artemis-main/artemis/tools/committee_tool.py +422 -0
  337. package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
  338. package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
  339. package/artemis-main/artemis/tools/explorer_tool.py +581 -0
  340. package/artemis-main/artemis/tools/history/__init__.py +398 -0
  341. package/artemis-main/artemis/tools/history/replay.py +120 -0
  342. package/artemis-main/artemis/tools/history/screenshot.py +139 -0
  343. package/artemis-main/artemis/tools/history/search.py +563 -0
  344. package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
  345. package/artemis-main/artemis/tools/index.py +47 -0
  346. package/artemis-main/artemis/tools/log_tool.py +108 -0
  347. package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
  348. package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
  349. package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
  350. package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
  351. package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
  352. package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
  353. package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
  354. package/artemis-main/artemis/tools/scratchpad.py +819 -0
  355. package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
  356. package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
  357. package/artemis-main/artemis/tools/video_tool.py +261 -0
  358. package/artemis-main/artemis/tools/wait_tool.py +97 -0
  359. package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
  360. package/artemis-main/artemis/utils/coordinates.py +606 -0
  361. package/artemis-main/artemis/utils/credentials_validator.py +201 -0
  362. package/artemis-main/artemis/utils/cv_canvas.py +189 -0
  363. package/artemis-main/artemis/utils/cython_compat.py +45 -0
  364. package/artemis-main/artemis/utils/element_hit_test.py +80 -0
  365. package/artemis-main/artemis/utils/image_diff.py +335 -0
  366. package/artemis-main/artemis/utils/image_hash.py +80 -0
  367. package/artemis-main/artemis/utils/notes.py +470 -0
  368. package/artemis-main/artemis/utils/ocr_api.py +105 -0
  369. package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
  370. package/artemis-main/artemis/utils/plan_grammar.py +513 -0
  371. package/artemis-main/artemis/utils/python_executor.py +213 -0
  372. package/artemis-main/artemis/utils/startup_progress.py +60 -0
  373. package/artemis-main/artemis/utils/task_tree.py +1174 -0
  374. package/artemis-main/artemis/utils/text.py +37 -0
  375. package/artemis-main/artemis/utils/ui_filter.py +651 -0
  376. package/artemis-main/artemis/utils/video.py +685 -0
  377. package/artemis-main/artemis/utils/visualization.py +802 -0
  378. package/artemis-main/config/artemis.example.jsonc +12 -0
  379. package/artemis-main/mcp_server/README.md +111 -0
  380. package/artemis-main/mcp_server/__init__.py +21 -0
  381. package/artemis-main/mcp_server/__main__.py +20 -0
  382. package/artemis-main/mcp_server/background/__init__.py +15 -0
  383. package/artemis-main/mcp_server/background/task_runner.py +453 -0
  384. package/artemis-main/mcp_server/base.py +49 -0
  385. package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
  386. package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
  387. package/artemis-main/mcp_server/notifiers/base.py +45 -0
  388. package/artemis-main/mcp_server/notifiers/composite.py +87 -0
  389. package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
  390. package/artemis-main/mcp_server/notifiers/file.py +69 -0
  391. package/artemis-main/mcp_server/notifiers/script.py +90 -0
  392. package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
  393. package/artemis-main/mcp_server/rules.md +68 -0
  394. package/artemis-main/mcp_server/server.py +106 -0
  395. package/artemis-main/mcp_server/tools/__init__.py +29 -0
  396. package/artemis-main/mcp_server/tools/device_state.py +107 -0
  397. package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
  398. package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
  399. package/artemis-main/mcp_server/tools/task_manager.py +627 -0
  400. package/artemis-main/mcp_server/tools/task_runner.py +548 -0
  401. package/artemis-main/mcp_server/utils/__init__.py +15 -0
  402. package/artemis-main/mcp_server/utils/device_utils.py +180 -0
  403. package/artemis-main/mcp_server/utils/env_utils.py +65 -0
  404. package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
  405. package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
  406. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
  407. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
  408. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
  409. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
  410. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
  411. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
  412. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
  413. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
  414. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
  415. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
  416. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
  417. package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
  418. package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
  419. package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
  420. package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
  421. package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
  422. package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
  423. package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
  424. package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
  425. package/artemis-main/packages/artemis-client/README.md +112 -0
  426. package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
  427. package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
  428. package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
  429. package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
  430. package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
  431. package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
  432. package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
  433. package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
  434. package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
  435. package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
  436. package/artemis-main/pyproject.toml +259 -0
  437. package/artemis-main/pyright-core.json +17 -0
  438. package/artemis-main/scripts/check_dependency_sources.py +68 -0
  439. package/artemis-main/scripts/install_deps.ps1 +520 -0
  440. package/artemis-main/scripts/install_deps.sh +533 -0
  441. package/artemis-main/scripts/quality_ratchet.py +97 -0
  442. package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
  443. package/artemis-main/scripts/run_edge_case_part.py +327 -0
  444. package/artemis-main/scripts/run_ui.py +242 -0
  445. package/artemis-main/scripts/start.ps1 +500 -0
  446. package/artemis-main/scripts/test_all_edge_cases.py +269 -0
  447. package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
  448. package/artemis-main/setup.py +164 -0
  449. package/artemis-main/start.bat +35 -0
  450. package/artemis-main/start.sh +423 -0
  451. package/artemis-main/third_party/mobile_use/LICENSE +190 -0
  452. package/artemis-main/third_party/mobile_use/METADATA +27 -0
  453. package/artemis-main/third_party/mobile_use/NOTICE +12 -0
  454. package/artemis-main/third_party/mobile_use/__init__.py +19 -0
  455. package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
  456. package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
  457. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
  458. package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
  459. package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
  460. package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
  461. package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
  462. package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
  463. package/artemis-main/third_party/mobile_use/config/output.py +184 -0
  464. package/artemis-main/third_party/mobile_use/context.py +119 -0
  465. package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
  466. package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
  467. package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
  468. package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
  469. package/artemis-main/third_party/mobile_use/graph.py +54 -0
  470. package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
  471. package/artemis-main/third_party/mobile_use/main.py +130 -0
  472. package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
  473. package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
  474. package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
  475. package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
  476. package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
  477. package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
  478. package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
  479. package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
  480. package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
  481. package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
  482. package/artemis-main/third_party/mobile_use/testing.py +59 -0
  483. package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
  484. package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
  485. package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
  486. package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
  487. package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
  488. package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
  489. package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
  490. package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
  491. package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
  492. package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
  493. package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
  494. package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
  495. package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
  496. package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
  497. package/artemis-main/uv.lock +4908 -0
  498. package/bin/android-midscene-automation.js +1 -5
  499. package/bin/artemis-config.mjs +15 -0
  500. package/bin/artemis-embedded.py +56 -0
  501. package/bin/start-artemis.mjs +30 -0
  502. package/bin/start-dev.mjs +112 -0
  503. package/bin/test_artemis_embedded.py +44 -0
  504. package/package.json +10 -8
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +13 -1
  507. package/src/App.vue +72 -23
  508. package/src/api.ts +5 -0
  509. package/src/appium-recorder/AppiumPage.vue +1 -1
  510. package/src/appium-recorder/components/ComponentInspector.vue +1 -1
  511. package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
  512. package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
  513. package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
  514. package/src/appium-recorder/flow-appearance.ts +4 -4
  515. package/src/components/AppSidebar.vue +8 -7
  516. package/src/components/MidsceneMaintenanceNotice.vue +17 -0
  517. package/src/components/analytics/AnalysisCharts.vue +4 -4
  518. package/src/components/analytics/ReportAnalysis.vue +1 -1
  519. package/src/components/generator/TestCaseFileUpload.vue +1 -1
  520. package/src/components/help/HelpCenter.vue +56 -37
  521. package/src/pages/AiGeneratorPage.vue +1 -1
  522. package/src/pages/ArtemisPage.vue +72 -0
  523. package/src/pages/ConfigPage.vue +18 -19
  524. package/src/style.css +369 -186
  525. package/src/types.ts +1 -1
@@ -0,0 +1,1015 @@
1
+ /**
2
+ * Copyright 2026 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { ActionParam, StepReplayFrame } from '../core/models/stream.model';
18
+ import { extractNumbersFromCoordinateValue, isPureDirectionString, parseSequenceCoordinates, unwrapTraceAction } from './image-overlay.util';
19
+ import { cleanErrorMessage, joinTargetDescriptions } from './tool-formatter.util';
20
+
21
+ /**
22
+ * Safely parse JSON string if it looks like an object/array, otherwise return original value
23
+ */
24
+ export function safeParseJson(val: any): any {
25
+ if (typeof val === 'string' && (val.trim().startsWith('{') || val.trim().startsWith('['))) {
26
+ try {
27
+ return JSON.parse(val);
28
+ } catch {
29
+ return val;
30
+ }
31
+ }
32
+ return val;
33
+ }
34
+
35
+ // WeakMap memo for parsed action objects to avoid repeated JSON.parse and object allocation across change detection cycles
36
+ const actionObjectCache = new WeakMap<object, any>();
37
+
38
+ /**
39
+ * Safely extract action object from possible array or nested structure
40
+ */
41
+ export function getActionObject(action: any): any {
42
+ if (!action) return null;
43
+ if (Array.isArray(action)) {
44
+ return action.length > 0 ? getActionObject(action[0]) : null;
45
+ }
46
+ if (typeof action === 'object') {
47
+ const cached = actionObjectCache.get(action);
48
+ if (cached !== undefined) return cached;
49
+ const res = unwrapTraceAction(action);
50
+ actionObjectCache.set(action, res);
51
+ return res;
52
+ }
53
+ return action;
54
+ }
55
+
56
+ /**
57
+ * Check if the action is a user interaction action on Android
58
+ */
59
+ export function isAndroidAction(action: any): boolean {
60
+ if (!action) return false;
61
+ let name = '';
62
+ if (typeof action === 'string') {
63
+ name = action.toLowerCase();
64
+ } else {
65
+ const act = getActionObject(action);
66
+ if (!act) return false;
67
+ name = (act.name || act.action || '').toLowerCase();
68
+ }
69
+ return [
70
+ 'tap', 'click', 'click_sequence', 'input', 'input_text', 'swipe', 'scroll', 'press_key',
71
+ 'press_home', 'press_back', 'launch_app', 'manage_app', 'open_app',
72
+ 'focus_and_input_text', 'focus_and_clear_text', 'long_press', 'long_press_on',
73
+ 'wait_for_delay', 'wait', 'drag', 'drag_and_drop'
74
+ ].includes(name);
75
+ }
76
+
77
+ /**
78
+ * Check if the action or tool is a task completion / submission report
79
+ */
80
+ export function isReportStatusAction(action: any): boolean {
81
+ if (!action) return false;
82
+ let name = '';
83
+ if (typeof action === 'string') {
84
+ name = action.toLowerCase();
85
+ } else {
86
+ const act = getActionObject(action);
87
+ if (!act) return false;
88
+ name = (act.name || act.action || '').toLowerCase();
89
+ }
90
+ name = name.replace(/^(_)?(self\.)?exec_/, '');
91
+ return name === 'report_task_status' || name === 'report_status' || name === 'submit_task_status';
92
+ }
93
+
94
+ /**
95
+ * Get status of report_task_status (completed / failed)
96
+ */
97
+ export function getReportStatusValue(action: any): string {
98
+ const act = getActionObject(action);
99
+ if (!act) return 'completed';
100
+ const args = act.args && typeof act.args === 'object' ? act.args : (act.payload?.args || act.payload || act);
101
+ const status = args.status || act.status;
102
+ if (typeof status === 'string') {
103
+ return status.toLowerCase();
104
+ }
105
+ return 'completed';
106
+ }
107
+
108
+ /**
109
+ * Get explanation of report_task_status
110
+ */
111
+ export function getReportStatusExplanation(action: any): string {
112
+ const act = getActionObject(action);
113
+ if (!act) return '';
114
+ const args = act.args && typeof act.args === 'object' ? act.args : (act.payload?.args || act.payload || act);
115
+ return args.explanation || args.reason || args.summary || act.explanation || act.reason || act.summary || '';
116
+ }
117
+
118
+ /**
119
+ * Get Material Symbol icon name for Android action
120
+ */
121
+ export function getActionIcon(action: any): string {
122
+ const act = getActionObject(action);
123
+ if (!act) return 'ads_click';
124
+ const name = (act.name || act.action || '').toLowerCase();
125
+ switch (name) {
126
+ case 'tap':
127
+ case 'click':
128
+ case 'click_sequence':
129
+ return 'ads_click';
130
+ case 'input':
131
+ case 'input_text':
132
+ case 'focus_and_input_text':
133
+ return 'keyboard';
134
+ case 'focus_and_clear_text':
135
+ case 'clear_text':
136
+ return 'backspace';
137
+ case 'swipe':
138
+ case 'scroll':
139
+ return 'swipe';
140
+ case 'drag':
141
+ case 'drag_and_drop':
142
+ return 'drag_indicator';
143
+ case 'press_key':
144
+ case 'press_home':
145
+ case 'press_back':
146
+ return 'keyboard_tab';
147
+ case 'launch_app':
148
+ case 'manage_app':
149
+ case 'open_app':
150
+ return 'open_in_new';
151
+ case 'long_press':
152
+ case 'long_press_on':
153
+ return 'touch_app';
154
+ case 'wait_for_delay':
155
+ case 'delay':
156
+ case 'wait':
157
+ return 'hourglass_empty';
158
+ default:
159
+ return 'settings';
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get human-readable title for Android actions
165
+ */
166
+ export function getActionTitle(action: any): string {
167
+ const act = getActionObject(action);
168
+ if (!act) return 'Action';
169
+ const name = (act.name || act.action || '').toLowerCase();
170
+ switch (name) {
171
+ case 'tap':
172
+ case 'click':
173
+ case 'tap_element':
174
+ case 'click_element':
175
+ return 'Tapping Element';
176
+ case 'input':
177
+ case 'input_text':
178
+ case 'focus_and_input_text':
179
+ return 'Entering Text';
180
+ case 'focus_and_clear_text':
181
+ case 'clear_text':
182
+ return 'Clearing Text';
183
+ case 'swipe':
184
+ case 'scroll': {
185
+ const actObj = getActionObject(action);
186
+ const args = actObj?.args && typeof actObj.args === 'object' ? actObj.args : {};
187
+ const dir = actObj?.direction || actObj?.gesture || args.direction || args.gesture || (typeof args.action === 'string' ? args.action : '') || (typeof actObj?.action === 'string' && actObj.action !== name ? actObj.action : '');
188
+ if (dir && isPureDirectionString(dir)) {
189
+ return `Swiping Screen (${String(dir).toUpperCase()})`;
190
+ }
191
+ return 'Swiping Screen';
192
+ }
193
+ case 'drag':
194
+ case 'drag_and_drop':
195
+ return 'Dragging Screen';
196
+ case 'press_key':
197
+ case 'press_home':
198
+ case 'press_back':
199
+ return 'Pressing Hardware Key';
200
+ case 'launch_app':
201
+ case 'open_app':
202
+ return 'Launching Application';
203
+ case 'stop_app':
204
+ case 'close_app':
205
+ return 'Stopping Application';
206
+ case 'manage_app': {
207
+ const actObj = getActionObject(action);
208
+ const actStr = (actObj?.action || actObj?.args?.action || '').toLowerCase();
209
+ if (actStr === 'launch') return 'Launching Application';
210
+ if (actStr === 'stop' || actStr === 'close') return 'Stopping Application';
211
+ return 'Managing Application';
212
+ }
213
+ case 'wait_for_delay':
214
+ case 'delay':
215
+ case 'wait':
216
+ return 'Waiting for Delay';
217
+ case 'long_press':
218
+ case 'long_press_on':
219
+ return 'Long Pressing Element';
220
+ case 'click_sequence':
221
+ return 'Clicking Sequence';
222
+ default:
223
+ return name.replace(/_/g, ' ').replace(/\b\w/g, (c: string) => c.toUpperCase());
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Get target text or element description for Android action
229
+ */
230
+ export function getActionTargetText(action: any): string {
231
+ const act = getActionObject(action);
232
+ if (!act) return '';
233
+ const name = (act.name || act.action || '').toLowerCase();
234
+ const args = act.args && typeof act.args === 'object' ? act.args : {};
235
+
236
+ if (name.includes('app')) {
237
+ const rawApp = act.app_name || act.package_name || act.app || args.app_name || args.package_name || args.app || '';
238
+ const app = rawApp ? (rawApp.charAt(0).toUpperCase() + rawApp.slice(1)) : '';
239
+ const rawAction = act.action || args.action || '';
240
+ const actionStr = rawAction ? (String(rawAction).charAt(0).toUpperCase() + String(rawAction).slice(1).toLowerCase()) : '';
241
+ if (actionStr && app) {
242
+ return `${actionStr} ${app}`;
243
+ }
244
+ return app || actionStr || '';
245
+ }
246
+ if (name.includes('delay') || name.includes('wait')) {
247
+ const ms = act.time_in_ms || act.delay_ms || act.delay_seconds || args.time_in_ms || args.delay_ms || args.delay_seconds || args.duration;
248
+ if (ms) return `${ms}ms`;
249
+ }
250
+ // Flash click_sequence carries one self-described target per point; render
251
+ // them in the same ' → ' chain that getActionCoords uses for the points.
252
+ if (name === 'click_sequence' || name === 'tap_sequence') {
253
+ const joined = joinTargetDescriptions(act.target_descriptions ?? args.target_descriptions);
254
+ if (joined) return joined;
255
+ }
256
+ // target_text is observed element text (index targets); target_description is
257
+ // what the model said it aimed at (coordinate targets). Same label either way.
258
+ return act.target_text || act.target_description || act.target_class || act.element_id
259
+ || args.target_text || args.target_description || args.element_id || '';
260
+ }
261
+
262
+ /**
263
+ * Get input label for Android action
264
+ */
265
+ export function getActionInputLabel(action: any): string {
266
+ const act = getActionObject(action);
267
+ if (!act) return 'Input';
268
+ const name = (act.name || act.action || '').toLowerCase();
269
+ if (name.includes('delay') || name.includes('wait')) {
270
+ return 'Duration';
271
+ }
272
+ if (name === 'swipe' || name === 'scroll' || name === 'drag' || name === 'drag_and_drop') {
273
+ const actObj = getActionObject(action);
274
+ const args = actObj?.args && typeof actObj.args === 'object' ? actObj.args : {};
275
+ const dir = actObj?.direction || actObj?.gesture || args.direction || args.gesture || (typeof args.action === 'string' ? args.action : '') || (typeof actObj?.action === 'string' && actObj.action !== name ? actObj.action : '');
276
+ if (dir && isPureDirectionString(dir)) {
277
+ return 'Direction';
278
+ }
279
+ return 'Input';
280
+ }
281
+ if (name === 'press_key' || name.includes('key')) {
282
+ return 'Key';
283
+ }
284
+ if (name === 'input_text' || name.includes('input')) {
285
+ return 'Input Text';
286
+ }
287
+ return 'Input';
288
+ }
289
+
290
+ /**
291
+ * Get input text or value for Android action
292
+ */
293
+ export function getActionInputText(action: any): string {
294
+ const act = getActionObject(action);
295
+ if (!act) return '';
296
+ const name = (act.name || act.action || '').toLowerCase();
297
+ const args = act.args && typeof act.args === 'object' ? act.args : {};
298
+
299
+ if (name === 'press_key' || name.includes('key')) {
300
+ return act.key || act.keycode || args.key || args.keycode || '';
301
+ }
302
+ if (name === 'swipe' || name === 'scroll' || name === 'drag' || name === 'drag_and_drop') {
303
+ const dir = act.direction || act.gesture || args.direction || args.gesture || (typeof args.action === 'string' ? args.action : '') || (typeof act.action === 'string' && act.action !== name ? act.action : '');
304
+ if (dir && isPureDirectionString(dir)) {
305
+ return String(dir).toUpperCase();
306
+ }
307
+ return '';
308
+ }
309
+ if (name.includes('delay') || name.includes('wait')) {
310
+ const ms = act.time_in_ms || act.delay_ms || act.delay_seconds || args.time_in_ms || args.delay_ms || args.delay_seconds || args.duration;
311
+ if (ms) return `${ms}ms`;
312
+ }
313
+ return act.text || act.input_text || args.text || args.input_text || '';
314
+ }
315
+
316
+ /**
317
+ * Get coordinates string representation for Android action
318
+ */
319
+ export function getActionCoords(action: any): string {
320
+ const act = getActionObject(action);
321
+ if (!act) return '';
322
+ const args = act.args && typeof act.args === 'object' ? act.args : {};
323
+ const name = (act.name || act.action || '').toLowerCase();
324
+
325
+ // Check sequence first if action is sequence or sequence arg is present
326
+ const isSequenceAction = name === 'click_sequence' || name === 'tap_sequence' || Boolean(act.sequence || args.sequence || act.normalized_sequence || args.normalized_sequence);
327
+ const rawSeq = act.normalized_sequence || args.normalized_sequence || act.sequence || args.sequence || act.targets || args.targets || (Array.isArray(act.coordinates) && Array.isArray(act.coordinates[0]) ? act.coordinates : null) || (Array.isArray(args.coordinates) && Array.isArray(args.coordinates[0]) ? args.coordinates : null);
328
+ const seqPoints = parseSequenceCoordinates(rawSeq);
329
+ if (seqPoints && seqPoints.length > 0 && (isSequenceAction || seqPoints.length > 1)) {
330
+ return seqPoints.map(pt => `[${pt[0]}, ${pt[1]}]`).join(' → ');
331
+ }
332
+
333
+ // Check normalized start and end first
334
+ const normStart = extractNumbersFromCoordinateValue(act.normalized_start_coordinates || args.normalized_start_coordinates);
335
+ const normEnd = extractNumbersFromCoordinateValue(act.normalized_end_coordinates || args.normalized_end_coordinates);
336
+ if (normStart && normEnd && normStart.length === 2 && normEnd.length === 2) {
337
+ return `[${normStart[0]}, ${normStart[1]}] → [${normEnd[0]}, ${normEnd[1]}]`;
338
+ }
339
+
340
+ const startCoords = extractNumbersFromCoordinateValue(act.start_coordinates || act.start_point || act.start || act.from || args.start_coordinates || args.start_point || args.start || args.from);
341
+ const endCoords = extractNumbersFromCoordinateValue(act.end_coordinates || act.end_point || act.end || act.to || args.end_coordinates || args.end_point || args.end || args.to);
342
+ if (startCoords && endCoords && startCoords.length === 2 && endCoords.length === 2) {
343
+ return `[${startCoords[0]}, ${startCoords[1]}] → [${endCoords[0]}, ${endCoords[1]}]`;
344
+ }
345
+
346
+ const normCoords = extractNumbersFromCoordinateValue(act.normalized_coordinates || args.normalized_coordinates);
347
+ const coords = normCoords ||
348
+ extractNumbersFromCoordinateValue(act.coordinates) ||
349
+ extractNumbersFromCoordinateValue(args.coordinates) ||
350
+ extractNumbersFromCoordinateValue(act.coords) ||
351
+ extractNumbersFromCoordinateValue(args.coords) ||
352
+ extractNumbersFromCoordinateValue(act.target) ||
353
+ extractNumbersFromCoordinateValue(args.target) ||
354
+ extractNumbersFromCoordinateValue(args.action) ||
355
+ extractNumbersFromCoordinateValue(act.action) ||
356
+ extractNumbersFromCoordinateValue(args.gesture) ||
357
+ extractNumbersFromCoordinateValue(act.gesture);
358
+
359
+ if (coords && Array.isArray(coords)) {
360
+ if (coords.length === 4) {
361
+ return `[${coords[0]}, ${coords[1]}] → [${coords[2]}, ${coords[3]}]`;
362
+ }
363
+ if (coords.length === 2) {
364
+ return `[${coords[0]}, ${coords[1]}]`;
365
+ }
366
+ return coords.join(', ');
367
+ }
368
+
369
+ return '';
370
+ }
371
+
372
+ /**
373
+ * The Validator records "Dispatched" as the terminal attempt of an action the device
374
+ * accepted (kept only when a retry preceded it); any other terminal attempt is the
375
+ * failure text. Skipped burst members count as not dispatched.
376
+ */
377
+ function attemptsFailed(attempts: any): boolean {
378
+ if (!Array.isArray(attempts) || attempts.length === 0) return false;
379
+ return String(attempts[attempts.length - 1]) !== 'Dispatched';
380
+ }
381
+
382
+ function lastFailedAttempt(attempts: any): string | null {
383
+ if (!Array.isArray(attempts)) return null;
384
+ const failing = attempts.filter((a: any) => String(a) !== 'Dispatched');
385
+ return failing.length > 0 ? String(failing[failing.length - 1]) : null;
386
+ }
387
+
388
+ /**
389
+ * Check if the action execution failed or encountered an execution failure/interception
390
+ */
391
+ export function isActionFailed(action: any, stepData?: any): boolean {
392
+ const act = getActionObject(action);
393
+ if (act) {
394
+ if (act.status === 'failed' || act.status === 'error' || act.success === false) return true;
395
+ }
396
+ if (stepData) {
397
+ if (stepData.status === 'failed' || stepData.status === 'error') return true;
398
+ if (stepData.last_execution_result) {
399
+ const res = typeof stepData.last_execution_result === 'string'
400
+ ? (() => { try { return JSON.parse(stepData.last_execution_result); } catch { return null; } })()
401
+ : stepData.last_execution_result;
402
+
403
+ if (res && typeof res === 'object') {
404
+ if (res.status === 'failed' || res.status === 'error' || res.success === false || res.is_successful === false) {
405
+ return true;
406
+ }
407
+ if (res.repair_status === 'fixed' || res.repair_status === 'failed' || res.repair_status === 'cannot_fix') {
408
+ return true;
409
+ }
410
+ if (Array.isArray(res.execution) && res.execution.length > 0) {
411
+ const firstExec = res.execution[0];
412
+ if (firstExec && (firstExec.status === 'failed' || firstExec.status === 'error' || firstExec.error || attemptsFailed(firstExec.attempts))) {
413
+ return true;
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+ return false;
420
+ }
421
+
422
+ /**
423
+ * Get failure message or reason for an action
424
+ */
425
+ export function getActionErrorMessage(action: any, stepData?: any): string {
426
+ const act = getActionObject(action);
427
+ if (act && (act.error || act.message || act.failure_reason)) {
428
+ return cleanErrorMessage(act.error || act.message || act.failure_reason);
429
+ }
430
+ if (stepData && stepData.last_execution_result) {
431
+ const res = typeof stepData.last_execution_result === 'string'
432
+ ? (() => { try { return JSON.parse(stepData.last_execution_result); } catch { return null; } })()
433
+ : stepData.last_execution_result;
434
+
435
+ if (res && typeof res === 'object') {
436
+ if (Array.isArray(res.execution) && res.execution.length > 0) {
437
+ const firstExec = res.execution[0];
438
+ if (firstExec) {
439
+ const failedAttempt = lastFailedAttempt(firstExec.attempts);
440
+ if (failedAttempt) {
441
+ return cleanErrorMessage(failedAttempt);
442
+ }
443
+ if (firstExec.error || firstExec.failure_reason || firstExec.message) {
444
+ return cleanErrorMessage(firstExec.error || firstExec.failure_reason || firstExec.message);
445
+ }
446
+ }
447
+ }
448
+ if (res.error || res.message || res.failure_reason) {
449
+ return cleanErrorMessage(res.error || res.message || res.failure_reason);
450
+ }
451
+ }
452
+ }
453
+ return 'Action Failed';
454
+ }
455
+
456
+ /**
457
+ * Get bounds string representation for Android action
458
+ */
459
+ export function getActionBounds(action: any): string {
460
+ const act = getActionObject(action);
461
+ if (!act) return '';
462
+ const bounds = act.target_bounds || act.bounds || (act.args && (act.args.target_bounds || act.args.bounds));
463
+ if (Array.isArray(bounds)) {
464
+ return bounds.join(', ');
465
+ }
466
+ return bounds ? String(bounds) : '';
467
+ }
468
+
469
+ /**
470
+ * Get resource id for Android action target
471
+ */
472
+ export function getActionResourceId(action: any): string {
473
+ const act = getActionObject(action);
474
+ if (!act) return '';
475
+ return act.target_resource_id || act.resource_id || (act.args && (act.args.target_resource_id || act.args.resource_id)) || '';
476
+ }
477
+
478
+ /**
479
+ * Get class name for Android action target
480
+ */
481
+ export function getActionClass(action: any): string {
482
+ const act = getActionObject(action);
483
+ if (!act) return '';
484
+ return act.target_class || act.class_name || (act.args && (act.args.target_class || act.args.class_name)) || '';
485
+ }
486
+
487
+ /**
488
+ * Extract extra parameter key-value pairs for Android actions
489
+ */
490
+ export function extractActionExtraParams(action: any, cache?: WeakMap<any, ActionParam[]>): ActionParam[] {
491
+ const act = getActionObject(action);
492
+ if (!act || typeof act !== 'object') return [];
493
+ if (cache && cache.has(act)) {
494
+ return cache.get(act)!;
495
+ }
496
+
497
+ const standardKeys = new Set([
498
+ 'action', 'name', 'type', 'target_text', 'target_description', 'target_descriptions',
499
+ 'text', 'input_text', 'target',
500
+ 'coordinates', 'coords', 'target_bounds', 'bounds', 'target_resource_id',
501
+ 'resource_id', 'target_class', 'class_name', 'normalized_coordinates',
502
+ 'normalized_start_coordinates', 'normalized_end_coordinates',
503
+ 'start_coordinates', 'end_coordinates', 'start', 'end', 'from', 'to',
504
+ 'pre_image_name', 'post_image_name', 'pre_screenshot', 'post_screenshot',
505
+ 'before_screenshot', 'after_screenshot', 'status', 'success', 'timestamp',
506
+ 'created_at', 'start_time', 'execution_id', 'controller', 'agent', 'session_id', 'step_id',
507
+ 'app_name', 'package_name', 'app', 'key', 'keycode', 'time_in_ms', 'delay_ms', 'delay_seconds', 'duration',
508
+ 'args', 'kwargs', 'parameters', 'extra_params', 'direction', 'gesture'
509
+ ]);
510
+
511
+ const result: ActionParam[] = [];
512
+
513
+ const rawArgs = act.args || act.Args || act.kwargs || act.parameters;
514
+ let parsedArgs: any = {};
515
+ if (rawArgs) {
516
+ if (typeof rawArgs === 'object') {
517
+ parsedArgs = rawArgs;
518
+ } else if (typeof rawArgs === 'string') {
519
+ try {
520
+ parsedArgs = JSON.parse(rawArgs);
521
+ } catch {
522
+ if (!rawArgs.includes('<') && !rawArgs.includes('object at')) {
523
+ parsedArgs = { details: rawArgs };
524
+ }
525
+ }
526
+ }
527
+ }
528
+
529
+ const mergedObj = { ...act, ...parsedArgs };
530
+
531
+ // Explicitly check duration if available and not already formatted
532
+ const dur = mergedObj.duration || mergedObj.duration_ms;
533
+ if (dur !== undefined && dur !== null && dur !== '') {
534
+ result.push({ key: 'Duration', value: typeof dur === 'number' ? `${dur}ms` : String(dur) });
535
+ }
536
+
537
+ for (const [k, v] of Object.entries(mergedObj)) {
538
+ const lowerK = k.toLowerCase();
539
+ if (standardKeys.has(lowerK)) continue;
540
+ if (v === null || v === undefined || v === '') continue;
541
+
542
+ let valStr = String(v);
543
+ if (valStr.includes('object at 0x') || valStr.startsWith('<artemis.') || valStr.includes('<controller')) continue;
544
+
545
+ if (typeof v === 'object') {
546
+ try { valStr = JSON.stringify(v); } catch { valStr = String(v); }
547
+ }
548
+
549
+ let prettyKey = k.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
550
+ if (lowerK === 'time_in_ms' || lowerK === 'delay_ms') prettyKey = 'Delay';
551
+
552
+ result.push({ key: prettyKey, value: valStr });
553
+ }
554
+
555
+ if (cache) {
556
+ cache.set(act, result);
557
+ }
558
+ return result;
559
+ }
560
+
561
+ /**
562
+ * Format image URL or path safely
563
+ */
564
+ export function formatImageUrl(candidate: any): string | null {
565
+ if (!candidate || candidate === 'None' || candidate === 'null' || candidate === 'undefined') return null;
566
+ if (typeof candidate !== 'string') return null;
567
+ const trimmed = candidate.trim();
568
+ if (!trimmed || trimmed === 'None' || trimmed === 'null' || trimmed === 'undefined') return null;
569
+
570
+ if (trimmed.startsWith('data:') || trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
571
+ return trimmed;
572
+ }
573
+ if (trimmed.startsWith('/local_file') || trimmed.startsWith('/images/') || trimmed.startsWith('/api/images/')) {
574
+ return trimmed;
575
+ }
576
+ if (trimmed.startsWith('file://')) {
577
+ return `/local_file?path=${encodeURIComponent(trimmed)}`;
578
+ }
579
+ if (trimmed.startsWith('/')) {
580
+ return `/local_file?path=${encodeURIComponent('file://' + trimmed)}`;
581
+ }
582
+ return `/images/${trimmed}`;
583
+ }
584
+
585
+ /**
586
+ * Helper to check if an item is a generic tool trace rather than the step's primary action
587
+ */
588
+ export function isGenericToolTrace(actionData: any, stepData?: any): boolean {
589
+ if (!actionData) return false;
590
+ if (actionData === stepData?.action_taken) return false;
591
+ if (Array.isArray(stepData?.action_taken)) {
592
+ if (stepData.action_taken === actionData || stepData.action_taken.includes(actionData)) {
593
+ return false;
594
+ }
595
+ }
596
+ if (actionData.is_primary_step_action) return false;
597
+ if (actionData.type === 'action' || isAndroidAction(actionData)) return false;
598
+ return Boolean(actionData.trace_id || actionData.payload || actionData.type === 'tool');
599
+ }
600
+
601
+ interface ResolvedImageEntry {
602
+ pre: string | null;
603
+ post: string | null;
604
+ hasExplicitPost: boolean;
605
+ }
606
+
607
+ const stepImageMapCache = new WeakMap<any, { key: string; map: Map<any, ResolvedImageEntry> }>();
608
+
609
+
610
+ export function extractItemPreImage(item: any): string | null {
611
+ const act = getActionObject(item);
612
+ if (!act) return null;
613
+ const payload = safeParseJson(act.payload);
614
+ const result = safeParseJson(act.result || payload?.result);
615
+ const args = safeParseJson(act.args || payload?.args);
616
+
617
+ const candidate =
618
+ result?.pre_image_name || result?.pre_screenshot_name || result?.pre_screenshot ||
619
+ payload?.pre_image_name || payload?.pre_screenshot_name || payload?.pre_screenshot ||
620
+ args?.pre_image_name || args?.pre_screenshot_name || args?.pre_screenshot ||
621
+ act.pre_image_name || act.pre_screenshot_name || act.pre_screenshot || act.before_screenshot || act.screenshot;
622
+
623
+ return candidate ? String(candidate) : null;
624
+ }
625
+
626
+ export function extractItemPostImage(item: any): string | null {
627
+ const act = getActionObject(item);
628
+ if (!act) return null;
629
+ const payload = safeParseJson(act.payload);
630
+ const result = safeParseJson(act.result || payload?.result);
631
+ const args = safeParseJson(act.args || payload?.args);
632
+
633
+ const candidate =
634
+ result?.post_image_name || result?.post_screenshot_name || result?.post_screenshot ||
635
+ payload?.post_image_name || payload?.post_screenshot_name || payload?.post_screenshot ||
636
+ args?.post_image_name || args?.post_screenshot_name || args?.post_screenshot ||
637
+ act.post_image_name || act.post_screenshot_name || act.post_screenshot || act.after_screenshot;
638
+
639
+ return candidate ? String(candidate) : null;
640
+ }
641
+
642
+ function getStepFailureScreenshot(stepData: any): string | null {
643
+ if (!stepData) return null;
644
+ if (stepData.failed_screenshot) return String(stepData.failed_screenshot);
645
+ if (stepData.failure_screenshot) return String(stepData.failure_screenshot);
646
+ if (stepData.failed_post_image_name) return String(stepData.failed_post_image_name);
647
+
648
+ if (Array.isArray(stepData.generic_tools)) {
649
+ for (const tool of stepData.generic_tools) {
650
+ const act = getActionObject(tool);
651
+ const payload = safeParseJson(act?.payload);
652
+ const args = safeParseJson(act?.args || payload?.args);
653
+ const name = String(act?.name || act?.agent_name || '').toLowerCase();
654
+ if (args?.post_screenshot_name) return String(args.post_screenshot_name);
655
+ if (args?.post_screenshot && (name.includes('failure') || name.includes('validator') || name.includes('diagnos'))) {
656
+ return String(args.post_screenshot);
657
+ }
658
+ if (act?.post_screenshot_name && (name.includes('failure') || name.includes('validator'))) {
659
+ return String(act.post_screenshot_name);
660
+ }
661
+ }
662
+ }
663
+ return null;
664
+ }
665
+
666
+ function getEventTimestamp(obj: any): number {
667
+ if (!obj) return 0;
668
+ const ts = obj.timestamp ?? obj.start_time ?? obj.created_at;
669
+ if (typeof ts === 'number') {
670
+ return ts < 1e11 ? ts * 1000 : ts;
671
+ }
672
+ if (typeof ts === 'string') {
673
+ const parsed = new Date(ts).getTime();
674
+ if (!isNaN(parsed) && parsed > 0) return parsed;
675
+ }
676
+ return 0;
677
+ }
678
+
679
+ export function resolveStepImageMap(stepData: any): Map<any, ResolvedImageEntry> {
680
+ const emptyMap = new Map<any, ResolvedImageEntry>();
681
+ if (!stepData || typeof stepData !== 'object') return emptyMap;
682
+
683
+ const toolsLen = Array.isArray(stepData.generic_tools) ? stepData.generic_tools.length : 0;
684
+ const toolsImageSig = Array.isArray(stepData.generic_tools)
685
+ ? stepData.generic_tools.map((t: any) => `${t?.trace_id || ''}:${t?.post_image_name || t?.post_screenshot || ''}`).join(',')
686
+ : '';
687
+ const cacheKey = `${stepData.step_id || ''}_${toolsLen}_${toolsImageSig}_${stepData.post_image_name || ''}_${stepData.pre_image_name || ''}`;
688
+
689
+ const cached = stepImageMapCache.get(stepData);
690
+ if (cached && cached.key === cacheKey) {
691
+ return cached.map;
692
+ }
693
+
694
+ const map = new Map<any, ResolvedImageEntry>();
695
+ const events: Array<{ item: any; act: any; timestamp: number; isPrimary: boolean }> = [];
696
+
697
+ const hasActionInTools = Array.isArray(stepData.generic_tools) && stepData.generic_tools.some(
698
+ (t: any) => t && (t.type === 'action' || isAndroidAction(t) || isReportStatusAction(t))
699
+ );
700
+
701
+ if (!hasActionInTools && stepData.action_taken) {
702
+ events.push({
703
+ item: stepData.action_taken,
704
+ act: getActionObject(stepData.action_taken),
705
+ timestamp: getEventTimestamp(stepData.action_taken),
706
+ isPrimary: true
707
+ });
708
+ }
709
+
710
+ if (Array.isArray(stepData.generic_tools)) {
711
+ stepData.generic_tools.forEach((t: any) => {
712
+ if (t) {
713
+ const isAction = t.type === 'action' || isAndroidAction(t) || isReportStatusAction(t);
714
+ events.push({
715
+ item: t,
716
+ act: getActionObject(t),
717
+ timestamp: getEventTimestamp(t),
718
+ isPrimary: isAction
719
+ });
720
+ }
721
+ });
722
+ }
723
+
724
+ events.sort((a, b) => a.timestamp - b.timestamp);
725
+
726
+ const failureShot = getStepFailureScreenshot(stepData);
727
+ const hadPrimaryFailure = Boolean(failureShot) || (stepData.action_taken && isActionFailed(stepData.action_taken, stepData));
728
+ const hasSubsequentActions = events.some(e => !e.isPrimary && (isAndroidAction(e.act) || Boolean(extractItemPostImage(e.act))));
729
+
730
+ let lastActionItem: any = null;
731
+ for (let i = events.length - 1; i >= 0; i--) {
732
+ const e = events[i];
733
+ if (isAndroidAction(e.act) || extractItemPostImage(e.act) || e.isPrimary) {
734
+ lastActionItem = e.item;
735
+ break;
736
+ }
737
+ }
738
+
739
+ let currentPreImage = stepData.pre_image_name || stepData.pre_screenshot || null;
740
+ let switchedToFailureShot = false;
741
+
742
+ for (const e of events) {
743
+ const act = e.act;
744
+ const explicitPre = extractItemPreImage(act);
745
+ const explicitPost = extractItemPostImage(act);
746
+
747
+ const agentName = String(
748
+ act?.agent_name || act?.agent || safeParseJson(act?.payload)?.agent_name || act?.name || ''
749
+ ).toLowerCase();
750
+ const isRecoveryOrValidator = agentName.includes('failure') || agentName.includes('analyzer') || agentName.includes('validator') || agentName.includes('diagnos');
751
+
752
+ if (failureShot && !switchedToFailureShot && (isRecoveryOrValidator || (!e.isPrimary && hadPrimaryFailure))) {
753
+ currentPreImage = failureShot;
754
+ switchedToFailureShot = true;
755
+ }
756
+
757
+ const resolvedPre = explicitPre || currentPreImage;
758
+ let resolvedPost: string | null = null;
759
+ let hasExplicitPost = false;
760
+
761
+ if (explicitPost) {
762
+ resolvedPost = explicitPost;
763
+ hasExplicitPost = true;
764
+ } else if (e.isPrimary && !hadPrimaryFailure) {
765
+ resolvedPost = stepData.post_image_name || stepData.post_screenshot || null;
766
+ } else if (e.item === lastActionItem && !hadPrimaryFailure) {
767
+ resolvedPost = stepData.post_image_name || stepData.post_screenshot || null;
768
+ } else if (e.item === lastActionItem && isRecoveryOrValidator) {
769
+ resolvedPost = stepData.post_image_name || stepData.post_screenshot || null;
770
+ }
771
+
772
+ // "The next pre-action state is the previous post-action state"
773
+ if (resolvedPost) {
774
+ currentPreImage = resolvedPost;
775
+ }
776
+
777
+ const entry: ResolvedImageEntry = { pre: resolvedPre, post: resolvedPost, hasExplicitPost };
778
+ map.set(e.item, entry);
779
+ if (act && act !== e.item) map.set(act, entry);
780
+ if (act?.trace_id) map.set(act.trace_id, entry);
781
+ if (e.item?.trace_id) map.set(e.item.trace_id, entry);
782
+ }
783
+
784
+ // Ensure stepData.action_taken cross-resolves to the action images
785
+ if (stepData.action_taken && lastActionItem) {
786
+ const lastEntry = map.get(lastActionItem);
787
+ if (lastEntry) {
788
+ map.set(stepData.action_taken, lastEntry);
789
+ const actObj = getActionObject(stepData.action_taken);
790
+ if (actObj && actObj !== stepData.action_taken) {
791
+ map.set(actObj, lastEntry);
792
+ }
793
+ }
794
+ }
795
+
796
+ stepImageMapCache.set(stepData, { key: cacheKey, map });
797
+ return map;
798
+ }
799
+
800
+ function lookupResolvedImages(stepData: any, actionData?: any): ResolvedImageEntry {
801
+ const map = resolveStepImageMap(stepData);
802
+ if (!actionData) {
803
+ return {
804
+ pre: stepData?.pre_image_name || stepData?.pre_screenshot || null,
805
+ post: stepData?.post_image_name || stepData?.post_screenshot || null,
806
+ hasExplicitPost: Boolean(stepData?.post_image_name || stepData?.post_screenshot)
807
+ };
808
+ }
809
+
810
+ const act = getActionObject(actionData);
811
+
812
+ if (map.has(actionData)) return map.get(actionData)!;
813
+ if (act && map.has(act)) return map.get(act)!;
814
+ if (act?.trace_id && map.has(act.trace_id)) return map.get(act.trace_id)!;
815
+
816
+ const explicitPre = extractItemPreImage(act);
817
+ const explicitPost = extractItemPostImage(act);
818
+ const pre = explicitPre || stepData?.pre_image_name || stepData?.pre_screenshot || null;
819
+ const post = explicitPost || (isGenericToolTrace(actionData, stepData) ? null : (stepData?.post_image_name || stepData?.post_screenshot || null));
820
+ return { pre, post, hasExplicitPost: Boolean(explicitPost) };
821
+ }
822
+
823
+ /**
824
+ * Get pre-action screenshot URL
825
+ */
826
+ export function getStepPreImageUrl(stepData: any, actionData?: any): string | null {
827
+ const resolved = lookupResolvedImages(stepData, actionData);
828
+ return formatImageUrl(resolved.pre);
829
+ }
830
+
831
+ /**
832
+ * Get post-action screenshot URL
833
+ */
834
+ export function getStepPostImageUrl(stepData: any, actionData?: any): string | null {
835
+ const resolved = lookupResolvedImages(stepData, actionData);
836
+ const postUrl = formatImageUrl(resolved.post);
837
+ const preUrl = formatImageUrl(resolved.pre);
838
+
839
+ if (postUrl && preUrl && postUrl === preUrl && !resolved.hasExplicitPost) {
840
+ return null;
841
+ }
842
+ return postUrl;
843
+ }
844
+
845
+ /**
846
+ * Extract an ordered array of visual StepReplayFrames from session logs or step blocks
847
+ */
848
+ export function extractStepReplayFrames(logsOrSteps: any[]): StepReplayFrame[] {
849
+ if (!Array.isArray(logsOrSteps) || logsOrSteps.length === 0) {
850
+ return [];
851
+ }
852
+
853
+ // Extract raw step data objects
854
+ const rawSteps: any[] = [];
855
+ for (const item of logsOrSteps) {
856
+ if (!item) continue;
857
+ if ((item.type === 'step_updated' || item.type === 'step_recorded' || item.type === 'step') && item.data) {
858
+ rawSteps.push(item.data);
859
+ } else if (item.step_number !== undefined || item.step_id !== undefined) {
860
+ rawSteps.push(item);
861
+ }
862
+ }
863
+
864
+ // Unified dual-indexed step store: correlates by step_id and step_number
865
+ const stepsList: any[] = [];
866
+ const idToIndex = new Map<string, number>();
867
+ const numToIndex = new Map<number, number>();
868
+
869
+ for (const step of rawSteps) {
870
+ const stepId = (step.step_id !== undefined && step.step_id !== null && String(step.step_id).trim() !== '')
871
+ ? String(step.step_id).trim()
872
+ : null;
873
+ const stepNum = (step.step_number !== undefined && step.step_number !== null)
874
+ ? Number(step.step_number)
875
+ : null;
876
+
877
+ let targetIndex = -1;
878
+ if (stepId && idToIndex.has(stepId)) {
879
+ targetIndex = idToIndex.get(stepId)!;
880
+ } else if (stepNum !== null && numToIndex.has(stepNum)) {
881
+ targetIndex = numToIndex.get(stepNum)!;
882
+ }
883
+
884
+ if (targetIndex >= 0) {
885
+ const existing = stepsList[targetIndex];
886
+
887
+ // When merging duplicate records (e.g. status reports vs real physical actions):
888
+ const existingAct = existing.action_taken;
889
+ const newAct = step.action_taken;
890
+ const preferExistingAction = existingAct && isAndroidAction(existingAct) && isReportStatusAction(newAct);
891
+ const preferNewAction = newAct && isAndroidAction(newAct) && isReportStatusAction(existingAct);
892
+
893
+ const mergedStepNum = (stepNum !== null) ? stepNum : existing.step_number;
894
+ const mergedStepId = existing.step_id || step.step_id;
895
+
896
+ const merged = {
897
+ ...existing,
898
+ ...step,
899
+ step_id: mergedStepId,
900
+ step_number: mergedStepNum,
901
+ pre_image_name: preferExistingAction
902
+ ? existing.pre_image_name
903
+ : (step.pre_image_name || existing.pre_image_name),
904
+ post_image_name: preferExistingAction
905
+ ? (existing.post_image_name || step.post_image_name)
906
+ : (step.post_image_name || existing.post_image_name),
907
+ pre_screenshot: preferExistingAction
908
+ ? existing.pre_screenshot
909
+ : (step.pre_screenshot || existing.pre_screenshot),
910
+ post_screenshot: preferExistingAction
911
+ ? (existing.post_screenshot || step.post_screenshot)
912
+ : (step.post_screenshot || existing.post_screenshot),
913
+ action_taken: preferExistingAction
914
+ ? existing.action_taken
915
+ : (preferNewAction ? newAct : (step.action_taken || existing.action_taken)),
916
+ last_execution_result: step.last_execution_result || existing.last_execution_result,
917
+ summary: preferExistingAction
918
+ ? (existing.summary || step.summary)
919
+ : (step.summary || existing.summary),
920
+ operator_raw_thinking: step.operator_raw_thinking || existing.operator_raw_thinking,
921
+ operator_native_thinking: step.operator_native_thinking || existing.operator_native_thinking,
922
+ generic_tools: step.generic_tools || existing.generic_tools,
923
+ timestamp: existing.timestamp || step.timestamp
924
+ };
925
+
926
+ stepsList[targetIndex] = merged;
927
+ if (mergedStepId) idToIndex.set(String(mergedStepId), targetIndex);
928
+ if (mergedStepNum !== undefined && mergedStepNum !== null) {
929
+ numToIndex.set(Number(mergedStepNum), targetIndex);
930
+ }
931
+ } else {
932
+ const newIndex = stepsList.length;
933
+ stepsList.push({ ...step });
934
+ if (stepId) idToIndex.set(stepId, newIndex);
935
+ if (stepNum !== null) numToIndex.set(stepNum, newIndex);
936
+ }
937
+ }
938
+
939
+ // Pre-resolve candidate images and actions for every step
940
+ interface VisualCandidate {
941
+ stepData: any;
942
+ act: any;
943
+ preUrl: string | null;
944
+ postUrl: string | null;
945
+ primaryImg: string;
946
+ }
947
+
948
+ const visualCandidates: VisualCandidate[] = [];
949
+
950
+ for (const stepData of stepsList) {
951
+ const act = stepData.action_taken || (Array.isArray(stepData.generic_tools)
952
+ ? stepData.generic_tools.find((t: any) => t && (t.type === 'action' || isAndroidAction(t)))
953
+ : null);
954
+ const preUrl = getStepPreImageUrl(stepData, act);
955
+ const postUrl = getStepPostImageUrl(stepData, act);
956
+ const primaryImg = preUrl || postUrl;
957
+
958
+ // Only steps with a resolvable screen image qualify as replay frames
959
+ if (primaryImg) {
960
+ visualCandidates.push({
961
+ stepData,
962
+ act,
963
+ preUrl,
964
+ postUrl,
965
+ primaryImg
966
+ });
967
+ }
968
+ }
969
+
970
+ // Sort strictly by original step_number ascending, then by timestamp
971
+ visualCandidates.sort((a, b) => {
972
+ const numA = Number(a.stepData.step_number ?? 99999);
973
+ const numB = Number(b.stepData.step_number ?? 99999);
974
+ if (numA !== numB) return numA - numB;
975
+ return Number(a.stepData.timestamp ?? 0) - Number(b.stepData.timestamp ?? 0);
976
+ });
977
+
978
+ // Construct guaranteed sequential 1-indexed frames
979
+ const frames: StepReplayFrame[] = [];
980
+
981
+ for (let i = 0; i < visualCandidates.length; i++) {
982
+ const { stepData, act, preUrl, postUrl, primaryImg } = visualCandidates[i];
983
+ const stepNum = i + 1; // 1-indexed sequential frame number (1, 2, 3...)
984
+ const title = act ? (getActionTitle(act) || 'Action') : `Step ${stepNum}`;
985
+ const coords = act ? getActionCoords(act) : '';
986
+ const targetText = act ? getActionTargetText(act) : '';
987
+ const actionDesc = coords ? `${title} (${coords})` : (targetText ? `${title} (${targetText})` : title);
988
+ const failed = isActionFailed(act, stepData);
989
+ const rawStepNum = (stepData.step_number !== undefined && stepData.step_number !== null)
990
+ ? Number(stepData.step_number)
991
+ : stepNum;
992
+
993
+ frames.push({
994
+ index: i,
995
+ stepNumber: stepNum,
996
+ rawStepNumber: rawStepNum,
997
+ stepId: String(stepData.step_id || `step-${stepNum}`),
998
+ title: `Step ${stepNum}: ${title}`,
999
+ actionText: actionDesc,
1000
+ action: act,
1001
+ actionType: act?.action || act?.name || 'action',
1002
+ targetText,
1003
+ coords,
1004
+ imageUrl: primaryImg,
1005
+ preImageUrl: preUrl,
1006
+ postImageUrl: postUrl,
1007
+ isPost: false,
1008
+ timestamp: stepData.timestamp,
1009
+ summary: stepData.summary || '',
1010
+ status: failed ? 'failed' : 'dispatched'
1011
+ });
1012
+ }
1013
+
1014
+ return frames;
1015
+ }