android-midscene-automation 0.2.4 → 1.1.1
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.
- package/CHANGELOG.md +10 -0
- package/README.md +159 -29
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- 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, '&')
|
|
249
|
+
.replace(/</g, '<')
|
|
250
|
+
.replace(/>/g, '>');
|
|
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('> ') || trimmed.startsWith('>')) {
|
|
312
|
+
closeList();
|
|
313
|
+
const quoteContent = formatInlineMarkdown(trimmed.replace(/^>\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
|
+
}
|