android-midscene-automation 0.2.4 → 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 +7 -0
  2. package/README.md +35 -0
  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 +4 -2
  505. package/server/artemis.ts +34 -0
  506. package/server/http-api.ts +12 -0
  507. package/src/App.vue +10 -5
  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 +340 -121
  525. package/src/types.ts +1 -1
@@ -0,0 +1,70 @@
1
+ import { parseNoteLines, parseNote, renderMarkdownToHtml } from './markdown-parser.util';
2
+
3
+ describe('markdown-parser.util verification & check lines', () => {
4
+ it('should parse - verify: line as type verify with checkKind verify', () => {
5
+ const markdown = `- [x] Parent task
6
+ - [x] Subtask 1
7
+ - verify: Commute duration is recorded in note \`eta_details\``;
8
+
9
+ const lines = parseNoteLines(markdown);
10
+ expect(lines.length).toBe(3);
11
+ expect(lines[0].type).toBe('checked');
12
+ expect(lines[1].type).toBe('checked');
13
+
14
+ const verifyLine = lines[2];
15
+ expect(verifyLine.type).toBe('verify');
16
+ expect(verifyLine.checkKind).toBe('verify');
17
+ expect(verifyLine.atEnd).toBe(false);
18
+ expect(verifyLine.segments.map(s => s.text).join('')).toBe('Commute duration is recorded in note eta_details');
19
+ expect(verifyLine.segments.find(s => s.code)?.text).toBe('eta_details');
20
+ });
21
+
22
+ it('should parse - assert: and - assert@end: lines', () => {
23
+ const markdown = `- [ ] Open app
24
+ - assert: the welcome screen shows up
25
+ - assert@end: final status is completed`;
26
+
27
+ const lines = parseNoteLines(markdown);
28
+ expect(lines.length).toBe(3);
29
+
30
+ expect(lines[1].type).toBe('verify');
31
+ expect(lines[1].checkKind).toBe('assert');
32
+ expect(lines[1].atEnd).toBe(false);
33
+
34
+ expect(lines[2].type).toBe('verify');
35
+ expect(lines[2].checkKind).toBe('assert');
36
+ expect(lines[2].atEnd).toBe(true);
37
+ });
38
+
39
+ it('should parse - finding: lines', () => {
40
+ const markdown = `- [x] Step 1
41
+ - finding: Unresolved verify failure`;
42
+
43
+ const lines = parseNoteLines(markdown);
44
+ expect(lines[1].type).toBe('finding');
45
+ expect(lines[1].checkKind).toBe('finding');
46
+ expect(lines[1].segments[0].text).toBe('Unresolved verify failure');
47
+ });
48
+
49
+ it('should group verify lines under parent milestone checks in parseNote', () => {
50
+ const note = `- [x] Open Maps
51
+ - [x] Search destination
52
+ - verify: Arrival time is visible in note \`eta\``;
53
+
54
+ const parsed = parseNote(note);
55
+ expect(parsed.milestones.length).toBe(1);
56
+ expect(parsed.milestones[0].subSteps.length).toBe(1);
57
+ expect(parsed.milestones[0].subSteps[0].type).toBe('checked');
58
+ expect(parsed.milestones[0].checks.length).toBe(1);
59
+ expect(parsed.milestones[0].checks[0].type).toBe('verify');
60
+ expect(parsed.milestones[0].checks[0].checkKind).toBe('verify');
61
+ });
62
+
63
+ it('should render verify badges in renderMarkdownToHtml', () => {
64
+ const text = `- verify: status is ok`;
65
+ const html = renderMarkdownToHtml(text);
66
+ expect(html).toContain('class="md-verify-item"');
67
+ expect(html).toContain('<span class="verify-badge">verify</span>');
68
+ expect(html).toContain('status is ok');
69
+ });
70
+ });
@@ -0,0 +1,383 @@
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 { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote } from '../core/models/markdown.model';
18
+ import { CheckerResult } from '../core/models/stream.model';
19
+
20
+ /**
21
+ * Split text into bold, code, and plain segments
22
+ */
23
+ export function parseMarkdownSegments(text: string): MarkdownSegment[] {
24
+ if (!text) return [];
25
+ const parts = text.split(/(\*\*.*?\*\*|`.*?`)/g);
26
+ return parts
27
+ .filter(part => part !== '')
28
+ .map(part => {
29
+ if (part.startsWith('**') && part.endsWith('**')) {
30
+ return {
31
+ text: part.slice(2, -2),
32
+ bold: true,
33
+ code: false
34
+ };
35
+ } else if (part.startsWith('`') && part.endsWith('`')) {
36
+ return {
37
+ text: part.slice(1, -1),
38
+ bold: false,
39
+ code: true
40
+ };
41
+ } else {
42
+ return {
43
+ text: part,
44
+ bold: false,
45
+ code: false
46
+ };
47
+ }
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Parse markdown note content into structured lines (checklists, headers, list items)
53
+ */
54
+ export function parseNoteLines(content: string): MarkdownLine[] {
55
+ if (!content) return [];
56
+ return content.split('\n').map(line => {
57
+ const trimmed = line.trim();
58
+ const indent = line.length - line.trimStart().length;
59
+
60
+ // Checkbox [x]
61
+ if (trimmed.startsWith('- [x] ') || trimmed.startsWith('- [X] ')) {
62
+ const rawText = trimmed.substring(6);
63
+ return { type: 'checked', segments: parseMarkdownSegments(rawText), indent };
64
+ }
65
+ // Active checkbox [/]
66
+ if (trimmed.startsWith('- [/] ')) {
67
+ const rawText = trimmed.substring(6);
68
+ return { type: 'progress', segments: parseMarkdownSegments(rawText), indent };
69
+ }
70
+ // Unchecked checkbox [ ]
71
+ if (trimmed.startsWith('- [ ] ')) {
72
+ const rawText = trimmed.substring(6);
73
+ return { type: 'unchecked', segments: parseMarkdownSegments(rawText), indent };
74
+ }
75
+ // Check lines (- verify: / - assert: / - verify@end: / - assert@end:)
76
+ const checkMatch = trimmed.match(/^[-*]\s*(verify|assert)(@end)?\s*:\s*(.*)$/i);
77
+ if (checkMatch) {
78
+ const kind = checkMatch[1].toLowerCase() as 'verify' | 'assert';
79
+ const atEnd = Boolean(checkMatch[2]);
80
+ const rawText = checkMatch[3].trim();
81
+ return {
82
+ type: 'verify',
83
+ checkKind: kind,
84
+ atEnd,
85
+ segments: parseMarkdownSegments(rawText),
86
+ indent
87
+ };
88
+ }
89
+ // System findings (- finding:)
90
+ const findingMatch = trimmed.match(/^[-*]\s*finding\s*:\s*(.*)$/i);
91
+ if (findingMatch) {
92
+ const rawText = findingMatch[1].trim();
93
+ return {
94
+ type: 'finding',
95
+ checkKind: 'finding',
96
+ segments: parseMarkdownSegments(rawText),
97
+ indent
98
+ };
99
+ }
100
+ // Regular list item
101
+ if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
102
+ const rawText = trimmed.substring(2);
103
+ return { type: 'list-item', segments: parseMarkdownSegments(rawText), indent };
104
+ }
105
+ // Headers
106
+ if (trimmed.startsWith('# ')) {
107
+ const rawText = trimmed.substring(2);
108
+ return { type: 'h1', segments: parseMarkdownSegments(rawText), indent };
109
+ }
110
+ if (trimmed.startsWith('## ')) {
111
+ const rawText = trimmed.substring(3);
112
+ return { type: 'h2', segments: parseMarkdownSegments(rawText), indent };
113
+ }
114
+ if (trimmed.startsWith('### ')) {
115
+ const rawText = trimmed.substring(4);
116
+ return { type: 'h3', segments: parseMarkdownSegments(rawText), indent };
117
+ }
118
+ // Default line
119
+ return trimmed ? { type: 'text', segments: parseMarkdownSegments(line), indent } : { type: 'empty', segments: [], indent: 0 };
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Group parsed markdown lines into title, milestones (checklists), and other lines
125
+ */
126
+ export function parseNote(content: string): ParsedNote {
127
+ const lines = parseNoteLines(content);
128
+ const milestones: NoteMilestone[] = [];
129
+ const otherLines: MarkdownLine[] = [];
130
+ let title: string | null = null;
131
+ let currentMilestone: (NoteMilestone & { _indent?: number }) | null = null;
132
+ let milestoneCount = 0;
133
+
134
+ for (const line of lines) {
135
+ if (line.type === 'empty') {
136
+ continue;
137
+ }
138
+
139
+ if ((line.type === 'h1' || line.type === 'h2') && !title) {
140
+ title = line.segments.map(s => s.text).join('');
141
+ continue;
142
+ }
143
+
144
+ const isChecklistItem = ['checked', 'progress', 'unchecked'].includes(line.type);
145
+
146
+ if (isChecklistItem && (!currentMilestone || line.indent <= (currentMilestone._indent ?? 0))) {
147
+ milestoneCount++;
148
+ currentMilestone = {
149
+ index: milestoneCount,
150
+ type: line.type as 'checked' | 'progress' | 'unchecked',
151
+ segments: line.segments,
152
+ subSteps: [],
153
+ checks: [],
154
+ _indent: line.indent
155
+ };
156
+ milestones.push(currentMilestone);
157
+ } else if (currentMilestone) {
158
+ if (line.indent > (currentMilestone._indent ?? 0)) {
159
+ if (line.type === 'verify' || line.type === 'assert' || line.type === 'finding') {
160
+ currentMilestone.checks.push(line);
161
+ } else {
162
+ currentMilestone.subSteps.push(line);
163
+ }
164
+ } else {
165
+ otherLines.push(line);
166
+ }
167
+ } else {
168
+ otherLines.push(line);
169
+ }
170
+ }
171
+
172
+ return {
173
+ title,
174
+ milestones,
175
+ otherLines
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Parse the Checker JSON response block
181
+ */
182
+ export function extractCheckerResult(text: string): CheckerResult | null {
183
+ if (!text) return null;
184
+ let cleanText = text.trim();
185
+ if (cleanText.includes('```json')) {
186
+ const parts = cleanText.split('```json');
187
+ if (parts.length > 1) {
188
+ cleanText = parts[1].split('```')[0].trim();
189
+ }
190
+ } else if (cleanText.includes('```')) {
191
+ const parts = cleanText.split('```');
192
+ if (parts.length > 1) {
193
+ cleanText = parts[1].split('```')[0].trim();
194
+ }
195
+ }
196
+
197
+ try {
198
+ const parsed = JSON.parse(cleanText);
199
+ if (parsed && typeof parsed === 'object' && 'success' in parsed) {
200
+ return {
201
+ success: Boolean(parsed.success),
202
+ reason: parsed.reason || 'No reason provided.'
203
+ };
204
+ }
205
+ } catch {
206
+ // Ignore
207
+ }
208
+ return null;
209
+ }
210
+
211
+ /**
212
+ * Helper to format inline markdown segments (bold, italic, code, strikethrough)
213
+ */
214
+ function formatInlineMarkdown(text: string): string {
215
+ if (!text) return '';
216
+ return text
217
+ // Inline code first (so asterisks inside code aren't formatted)
218
+ .replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
219
+ // Bold + Italic
220
+ .replace(/\*\*\*([^*]+)\*\*\*/g, '<strong><em>$1</em></strong>')
221
+ .replace(/___([^_]+)___/g, '<strong><em>$1</em></strong>')
222
+ // Bold
223
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
224
+ .replace(/__([^_]+)__/g, '<strong>$1</strong>')
225
+ // Italic
226
+ .replace(/\*([^*]+)\*/g, '<em>$1</em>')
227
+ .replace(/\b_([^_]+)_\b/g, '<em>$1</em>')
228
+ // Strikethrough
229
+ .replace(/~~([^~]+)~~/g, '<del>$1</del>');
230
+ }
231
+
232
+ /**
233
+ * Convert basic markdown text to safe HTML for agent logs/thinking
234
+ */
235
+ export function renderMarkdownToHtml(text: string): string {
236
+ if (!text) return '';
237
+
238
+ // Clean up internal agent XML-like tags (e.g. <thought>, <short_term_memory>, etc.)
239
+ const cleanedText = text
240
+ .replace(/<\/?(thought|short_term_memory|strong_term_memory|reasoning|plan|task_plan|call_tool)[^\n>]*>?/gi, '')
241
+ .replace(/\r\n/g, '\n')
242
+ .replace(/\r/g, '\n')
243
+ .replace(/\n{3,}/g, '\n\n')
244
+ .trim();
245
+
246
+ // Escape HTML first to prevent XSS
247
+ const escaped = cleanedText
248
+ .replace(/&/g, '&amp;')
249
+ .replace(/</g, '&lt;')
250
+ .replace(/>/g, '&gt;');
251
+
252
+ const rawLines = escaped.split('\n');
253
+ const result: string[] = [];
254
+ let currentListType: 'ul' | 'ol' | null = null;
255
+ let inCodeBlock = false;
256
+ let codeBlockLang = '';
257
+ let codeBlockLines: string[] = [];
258
+
259
+ const closeList = () => {
260
+ if (currentListType) {
261
+ result.push(`</${currentListType}>`);
262
+ currentListType = null;
263
+ }
264
+ };
265
+
266
+ for (let i = 0; i < rawLines.length; i++) {
267
+ const rawLine = rawLines[i];
268
+ const trimmed = rawLine.trim();
269
+
270
+ // 1. Code blocks: ```lang ... ```
271
+ if (trimmed.startsWith('```')) {
272
+ if (inCodeBlock) {
273
+ // End code block
274
+ const codeContent = codeBlockLines.join('\n');
275
+ result.push(`<pre class="code-block"><code class="lang-${codeBlockLang}">${codeContent}</code></pre>`);
276
+ inCodeBlock = false;
277
+ codeBlockLines = [];
278
+ codeBlockLang = '';
279
+ } else {
280
+ closeList();
281
+ inCodeBlock = true;
282
+ codeBlockLang = trimmed.substring(3).trim();
283
+ codeBlockLines = [];
284
+ }
285
+ continue;
286
+ }
287
+
288
+ if (inCodeBlock) {
289
+ codeBlockLines.push(rawLine);
290
+ continue;
291
+ }
292
+
293
+ // 2. Headings: # to ######
294
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
295
+ if (headingMatch) {
296
+ closeList();
297
+ const level = headingMatch[1].length;
298
+ const headingContent = formatInlineMarkdown(headingMatch[2]);
299
+ result.push(`<h${level}>${headingContent}</h${level}>`);
300
+ continue;
301
+ }
302
+
303
+ // 3. Horizontal Rule: ---, ***, ___
304
+ if (/^(\-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
305
+ closeList();
306
+ result.push('<hr class="md-hr" />');
307
+ continue;
308
+ }
309
+
310
+ // 4. Blockquotes: > quote
311
+ if (trimmed.startsWith('&gt; ') || trimmed.startsWith('&gt;')) {
312
+ closeList();
313
+ const quoteContent = formatInlineMarkdown(trimmed.replace(/^&gt;\s?/, ''));
314
+ result.push(`<blockquote>${quoteContent}</blockquote>`);
315
+ continue;
316
+ }
317
+
318
+ // 5. Unordered List: - item or * item
319
+ const ulMatch = rawLine.match(/^(\s*)([\*\-])\s+(.*)$/);
320
+ if (ulMatch) {
321
+ if (currentListType !== 'ul') {
322
+ closeList();
323
+ currentListType = 'ul';
324
+ result.push('<ul>');
325
+ }
326
+ const spaces = ulMatch[1].length;
327
+ const subLevel = Math.max(0, Math.floor((spaces - 2) / 2));
328
+ const indent = subLevel * 12;
329
+ const style = indent ? ` style="margin-left: ${indent}px"` : '';
330
+ const itemText = ulMatch[3];
331
+ const checkMatch = itemText.match(/^(verify|assert)(@end)?\s*:\s*(.*)$/i);
332
+ const findingMatch = itemText.match(/^finding\s*:\s*(.*)$/i);
333
+ if (checkMatch) {
334
+ const kind = checkMatch[1].toLowerCase();
335
+ const atEnd = checkMatch[2] ? '@end' : '';
336
+ const assertClass = kind === 'assert' ? ' assert' : '';
337
+ const itemContent = formatInlineMarkdown(checkMatch[3].trim());
338
+ result.push(`<li${style} class="md-verify-item"><span class="verify-badge${assertClass}">${kind}${atEnd}</span>${itemContent}</li>`);
339
+ } else if (findingMatch) {
340
+ const itemContent = formatInlineMarkdown(findingMatch[1].trim());
341
+ result.push(`<li${style} class="md-finding-item"><span class="finding-badge">finding</span>${itemContent}</li>`);
342
+ } else {
343
+ const itemContent = formatInlineMarkdown(itemText);
344
+ result.push(`<li${style}>${itemContent}</li>`);
345
+ }
346
+ continue;
347
+ }
348
+
349
+ // 6. Ordered List: 1. item
350
+ const olMatch = rawLine.match(/^(\s*)(\d+)\.\s+(.*)$/);
351
+ if (olMatch) {
352
+ if (currentListType !== 'ol') {
353
+ closeList();
354
+ currentListType = 'ol';
355
+ result.push('<ol>');
356
+ }
357
+ const spaces = olMatch[1].length;
358
+ const subLevel = Math.max(0, Math.floor((spaces - 2) / 2));
359
+ const indent = subLevel * 12;
360
+ const style = indent ? ` style="margin-left: ${indent}px"` : '';
361
+ const itemContent = formatInlineMarkdown(olMatch[3]);
362
+ result.push(`<li${style}>${itemContent}</li>`);
363
+ continue;
364
+ }
365
+
366
+ // 7. Regular paragraph / empty line
367
+ closeList();
368
+ if (trimmed === '') {
369
+ result.push('<div class="md-para-gap"></div>');
370
+ } else {
371
+ const lineContent = formatInlineMarkdown(rawLine);
372
+ result.push(`<div>${lineContent}</div>`);
373
+ }
374
+ }
375
+
376
+ if (inCodeBlock) {
377
+ const codeContent = codeBlockLines.join('\n');
378
+ result.push(`<pre class="code-block"><code class="lang-${codeBlockLang}">${codeContent}</code></pre>`);
379
+ }
380
+
381
+ closeList();
382
+ return result.join('\n');
383
+ }
@@ -0,0 +1,107 @@
1
+ import {
2
+ hasSessionOffsets,
3
+ locateSessionTime,
4
+ locateTimelineTime,
5
+ sessionTimeToTimelineTime,
6
+ TimelineSegment
7
+ } from './recording-timeline.util';
8
+
9
+ describe('recording-timeline.util', () => {
10
+ // Recording started 1.2s after the session began. Segment 0 ran 10s, then
11
+ // scrcpy restarted for rotation (1.5s gap), segment 1 ran 5s, another 0.8s
12
+ // gap, and segment 2 ran 4s.
13
+ const v2Segments: TimelineSegment[] = [
14
+ { start: 0, duration: 10, offset_ms: 1200, duration_ms: 10000 },
15
+ { start: 10, duration: 5, offset_ms: 12700, duration_ms: 5000 },
16
+ { start: 15, duration: 4, offset_ms: 18500, duration_ms: 4000 }
17
+ ];
18
+
19
+ const legacySegments: TimelineSegment[] = [
20
+ { start: 0, duration: 10 },
21
+ { start: 10, duration: 5 }
22
+ ];
23
+
24
+ describe('hasSessionOffsets', () => {
25
+ it('detects manifest v2 segments and rejects legacy or mixed lists', () => {
26
+ expect(hasSessionOffsets(v2Segments)).toBeTrue();
27
+ expect(hasSessionOffsets(legacySegments)).toBeFalse();
28
+ expect(hasSessionOffsets([v2Segments[0], legacySegments[0]])).toBeFalse();
29
+ expect(hasSessionOffsets([])).toBeFalse();
30
+ });
31
+ });
32
+
33
+ describe('locateTimelineTime', () => {
34
+ it('maps back-to-back playlist times into segments', () => {
35
+ expect(locateTimelineTime(legacySegments, 3)).toEqual({ index: 0, localTime: 3, timelineTime: 3 });
36
+ expect(locateTimelineTime(legacySegments, 10)).toEqual({ index: 1, localTime: 0, timelineTime: 10 });
37
+ expect(locateTimelineTime(legacySegments, 12.5)).toEqual({ index: 1, localTime: 2.5, timelineTime: 12.5 });
38
+ });
39
+
40
+ it('clamps out-of-range times into the first or last segment', () => {
41
+ expect(locateTimelineTime(legacySegments, -4)).toEqual({ index: 0, localTime: 0, timelineTime: 0 });
42
+ expect(locateTimelineTime(legacySegments, 99)).toEqual({ index: 1, localTime: 5, timelineTime: 15 });
43
+ expect(locateTimelineTime([], 3)).toBeNull();
44
+ });
45
+ });
46
+
47
+ describe('locateSessionTime', () => {
48
+ it('maps a session time inside a segment using its real offset', () => {
49
+ // T+4.2s is 3.0s into segment 0 (which started at T+1.2s).
50
+ const inFirst = locateSessionTime(v2Segments, 4.2)!;
51
+ expect(inFirst.index).toBe(0);
52
+ expect(inFirst.localTime).toBeCloseTo(3.0, 6);
53
+ expect(inFirst.timelineTime).toBeCloseTo(3.0, 6);
54
+
55
+ // T+14.7s is 2.0s into segment 1 (started at T+12.7s), which sits at
56
+ // timeline 10s, so it must NOT drift to timeline 14.7s.
57
+ const inSecond = locateSessionTime(v2Segments, 14.7)!;
58
+ expect(inSecond.index).toBe(1);
59
+ expect(inSecond.localTime).toBeCloseTo(2.0, 6);
60
+ expect(inSecond.timelineTime).toBeCloseTo(12.0, 6);
61
+
62
+ const inThird = locateSessionTime(v2Segments, 20.5)!;
63
+ expect(inThird.index).toBe(2);
64
+ expect(inThird.localTime).toBeCloseTo(2.0, 6);
65
+ expect(inThird.timelineTime).toBeCloseTo(17.0, 6);
66
+ });
67
+
68
+ it('snaps a time inside a restart gap to the start of the next segment', () => {
69
+ // Segment 0 ends at T+11.2s, segment 1 starts at T+12.7s.
70
+ expect(locateSessionTime(v2Segments, 11.9)).toEqual({ index: 1, localTime: 0, timelineTime: 10 });
71
+ // Segment 1 ends at T+17.7s, segment 2 starts at T+18.5s.
72
+ expect(locateSessionTime(v2Segments, 18.0)).toEqual({ index: 2, localTime: 0, timelineTime: 15 });
73
+ });
74
+
75
+ it('treats segment boundaries as inclusive of the end frame', () => {
76
+ const atEnd = locateSessionTime(v2Segments, 11.2)!;
77
+ expect(atEnd.index).toBe(0);
78
+ expect(atEnd.localTime).toBeCloseTo(10, 6);
79
+ expect(locateSessionTime(v2Segments, 12.7)).toEqual({ index: 1, localTime: 0, timelineTime: 10 });
80
+ });
81
+
82
+ it('snaps times before the first frame and after the last frame', () => {
83
+ expect(locateSessionTime(v2Segments, 0)).toEqual({ index: 0, localTime: 0, timelineTime: 0 });
84
+ expect(locateSessionTime(v2Segments, 0.5)).toEqual({ index: 0, localTime: 0, timelineTime: 0 });
85
+ expect(locateSessionTime(v2Segments, 60)).toEqual({ index: 2, localTime: 4, timelineTime: 19 });
86
+ expect(locateSessionTime(v2Segments, Number.NaN)).toEqual({ index: 0, localTime: 0, timelineTime: 0 });
87
+ });
88
+
89
+ it('falls back to back-to-back timeline behaviour for legacy manifests', () => {
90
+ expect(locateSessionTime(legacySegments, 12.5)).toEqual({ index: 1, localTime: 2.5, timelineTime: 12.5 });
91
+ expect(locateSessionTime([], 5)).toBeNull();
92
+ });
93
+ });
94
+
95
+ describe('sessionTimeToTimelineTime', () => {
96
+ it('returns the playlist position for a session time', () => {
97
+ expect(sessionTimeToTimelineTime(v2Segments, 14.7)).toBeCloseTo(12.0, 6);
98
+ expect(sessionTimeToTimelineTime(v2Segments, 11.9)).toBe(10);
99
+ expect(sessionTimeToTimelineTime(legacySegments, 7)).toBe(7);
100
+ });
101
+
102
+ it('passes the time through when no playlist is available', () => {
103
+ expect(sessionTimeToTimelineTime([], 7)).toBe(7);
104
+ expect(sessionTimeToTimelineTime([], -2)).toBe(0);
105
+ });
106
+ });
107
+ });
@@ -0,0 +1,123 @@
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
+ /**
18
+ * Pure helpers that map times onto the multi-segment screen recording.
19
+ *
20
+ * A recording is split into segments whenever scrcpy restarts (device
21
+ * rotation, crash recovery). The player plays them back to back on a
22
+ * "timeline" axis (`start`/`duration`, no gaps), while step timestamps live
23
+ * on the "session" axis (seconds since the DataEngine session start). Manifest
24
+ * v2 segments carry `offset_ms`/`duration_ms` on the session axis, which lets
25
+ * a session time land in the right segment even after each restart gap.
26
+ */
27
+
28
+ export interface TimelineSegment {
29
+ /** Back-to-back playlist start in seconds. */
30
+ start: number;
31
+ /** Segment length in seconds. */
32
+ duration: number;
33
+ /** Session-relative first-frame offset in milliseconds (manifest v2). */
34
+ offset_ms?: number;
35
+ /** Segment length in milliseconds (manifest v2). */
36
+ duration_ms?: number;
37
+ }
38
+
39
+ export interface SegmentLocation {
40
+ /** Index into the segment list. */
41
+ index: number;
42
+ /** Seconds into that segment's own media. */
43
+ localTime: number;
44
+ /** Equivalent position on the back-to-back playlist axis, in seconds. */
45
+ timelineTime: number;
46
+ }
47
+
48
+ const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
49
+
50
+ /** True when every segment carries the session-relative v2 fields. */
51
+ export function hasSessionOffsets(segments: readonly TimelineSegment[]): boolean {
52
+ return segments.length > 0 && segments.every(
53
+ (segment) => Number.isFinite(segment.offset_ms) && Number.isFinite(segment.duration_ms)
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Locate a position on the back-to-back playlist axis (what the scrubber uses).
59
+ * Times past the end clamp into the last segment.
60
+ */
61
+ export function locateTimelineTime(
62
+ segments: readonly TimelineSegment[],
63
+ timelineSeconds: number
64
+ ): SegmentLocation | null {
65
+ if (!segments.length) return null;
66
+ const total = segments.reduce((sum, segment) => sum + Math.max(0, segment.duration), 0);
67
+ const target = clamp(Number.isFinite(timelineSeconds) ? timelineSeconds : 0, 0, total);
68
+ let index = segments.findIndex((segment) => target < segment.start + segment.duration);
69
+ if (index < 0) index = segments.length - 1;
70
+ const segment = segments[index];
71
+ const localTime = clamp(target - segment.start, 0, Math.max(0, segment.duration));
72
+ return { index, localTime, timelineTime: segment.start + localTime };
73
+ }
74
+
75
+ /**
76
+ * Locate a session-relative time (seconds since session start, the axis step
77
+ * timestamps use). A time inside `[offset, offset + duration]` of a segment maps
78
+ * into that segment; a time in the gap between two segments (scrcpy restart)
79
+ * snaps to the start of the next segment; a time before the first frame snaps
80
+ * to the first frame; a time after the last frame clamps to the last frame.
81
+ *
82
+ * Segments without session offsets (legacy manifests) are treated as if the
83
+ * session axis were the back-to-back timeline axis.
84
+ */
85
+ export function locateSessionTime(
86
+ segments: readonly TimelineSegment[],
87
+ sessionSeconds: number
88
+ ): SegmentLocation | null {
89
+ if (!segments.length) return null;
90
+ if (!hasSessionOffsets(segments)) {
91
+ return locateTimelineTime(segments, sessionSeconds);
92
+ }
93
+ const targetMs = Number.isFinite(sessionSeconds) ? sessionSeconds * 1000 : 0;
94
+ for (let index = 0; index < segments.length; index++) {
95
+ const segment = segments[index];
96
+ const offsetMs = segment.offset_ms as number;
97
+ const endMs = offsetMs + (segment.duration_ms as number);
98
+ if (targetMs < offsetMs) {
99
+ // Before this segment's first frame: either before the recording began
100
+ // or inside the restart gap after the previous segment. Snap forward.
101
+ return { index, localTime: 0, timelineTime: segment.start };
102
+ }
103
+ if (targetMs <= endMs) {
104
+ const localTime = clamp((targetMs - offsetMs) / 1000, 0, Math.max(0, segment.duration));
105
+ return { index, localTime, timelineTime: segment.start + localTime };
106
+ }
107
+ }
108
+ const lastIndex = segments.length - 1;
109
+ const last = segments[lastIndex];
110
+ const localTime = Math.max(0, last.duration);
111
+ return { index: lastIndex, localTime, timelineTime: last.start + localTime };
112
+ }
113
+
114
+ /**
115
+ * Convert a session-relative time into the back-to-back playlist time the
116
+ * player scrubber understands. Returns 0 when there are no segments.
117
+ */
118
+ export function sessionTimeToTimelineTime(
119
+ segments: readonly TimelineSegment[],
120
+ sessionSeconds: number
121
+ ): number {
122
+ return locateSessionTime(segments, sessionSeconds)?.timelineTime ?? Math.max(0, sessionSeconds || 0);
123
+ }