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
package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts
ADDED
|
@@ -0,0 +1,2235 @@
|
|
|
1
|
+
import { TranslatePipe } from '../../core/i18n/translate.pipe';
|
|
2
|
+
/**
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { Component, ChangeDetectionStrategy, NgZone, signal, computed, effect, inject, untracked, DestroyRef, AfterViewInit } from '@angular/core';
|
|
19
|
+
import { CommonModule } from '@angular/common';
|
|
20
|
+
import { FormsModule } from '@angular/forms';
|
|
21
|
+
import { RouterLink } from '@angular/router';
|
|
22
|
+
import { HttpClient } from '@angular/common/http';
|
|
23
|
+
import { AgentService, StartupProgressEvent } from '../../services/agent.service';
|
|
24
|
+
import { Session, ModelInfo, SessionUsage } from '../../core/models/session.model';
|
|
25
|
+
import { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote } from '../../core/models/markdown.model';
|
|
26
|
+
import { OverlayModule, ConnectedPosition } from '@angular/cdk/overlay';
|
|
27
|
+
import { StepBlock, PhaseBlock, StepEvent, ActionParam, CheckerResult, StreamResetNotice, DEFAULT_STREAM_RESET_MESSAGE } from '../../core/models/stream.model';
|
|
28
|
+
import {
|
|
29
|
+
contextPercent,
|
|
30
|
+
formatCompactTokens,
|
|
31
|
+
formatElapsed,
|
|
32
|
+
parseRunTuning,
|
|
33
|
+
sessionElapsedSeconds,
|
|
34
|
+
tuningLabel
|
|
35
|
+
} from '../../utils/run-info.util';
|
|
36
|
+
|
|
37
|
+
export const PLANNING_LOADER_PHRASES: string[] = [
|
|
38
|
+
'Planning next step...',
|
|
39
|
+
'Analyzing screen coordinates...',
|
|
40
|
+
'Consulting neural network...',
|
|
41
|
+
'Formulating tactical action plan...',
|
|
42
|
+
'Deciphering UI state & elements...',
|
|
43
|
+
'Synthesizing decision pathways...',
|
|
44
|
+
'Calibrating next move...',
|
|
45
|
+
'Aligning logical vectors...',
|
|
46
|
+
'Evaluating optimal sub-goals...',
|
|
47
|
+
'Gathering sensory inputs...',
|
|
48
|
+
'Computing next interaction...',
|
|
49
|
+
'Optimizing execution strategy...',
|
|
50
|
+
'Simulating probable outcomes...',
|
|
51
|
+
'Summoning AI intuition...',
|
|
52
|
+
'Brewing the next command...',
|
|
53
|
+
'Strategizing tactical moves...'
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export interface StartupWorkItem extends StartupProgressEvent {
|
|
57
|
+
isActive: boolean;
|
|
58
|
+
elapsed: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface StartupWorkStage {
|
|
62
|
+
started: string;
|
|
63
|
+
completed: string;
|
|
64
|
+
completedMessage: string;
|
|
65
|
+
/** Sub-steps that replace the live message while the stage is still running. */
|
|
66
|
+
liveDetailStages?: string[];
|
|
67
|
+
/** A later event whose message is a better completion line than the stage's own. */
|
|
68
|
+
completedDetailStage?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const STARTUP_WORK_STAGES: StartupWorkStage[] = [
|
|
72
|
+
{
|
|
73
|
+
started: 'device_check',
|
|
74
|
+
completed: 'device_ready',
|
|
75
|
+
completedMessage: 'Android device connected'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
started: 'uiautomator',
|
|
79
|
+
completed: 'uiautomator_ready',
|
|
80
|
+
completedMessage: 'UI hierarchy service is ready',
|
|
81
|
+
// First task on a device installs / upgrades the accessibility helper
|
|
82
|
+
// (a few seconds): say so instead of a generic "connecting".
|
|
83
|
+
liveDetailStages: ['helper_install', 'helper_upgrade'],
|
|
84
|
+
// "UI hierarchy source: Artemis accessibility helper v1.1.3" (or UIAutomator2).
|
|
85
|
+
completedDetailStage: 'hierarchy_backend'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
started: 'environment',
|
|
89
|
+
completed: 'environment_ready',
|
|
90
|
+
completedMessage: 'Device environment is ready'
|
|
91
|
+
}
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
function formatStartupElapsed(seconds: number): string {
|
|
95
|
+
const safeSeconds = Math.max(0, seconds);
|
|
96
|
+
return safeSeconds < 10
|
|
97
|
+
? `${safeSeconds.toFixed(1)}s`
|
|
98
|
+
: `${Math.round(safeSeconds)}s`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Collapse noisy process-level startup events into the three device preparation
|
|
103
|
+
* operations that are useful to someone watching a run.
|
|
104
|
+
*/
|
|
105
|
+
export function buildStartupWorkItems(
|
|
106
|
+
events: StartupProgressEvent[],
|
|
107
|
+
nowSeconds: number,
|
|
108
|
+
executionHasOutput: boolean,
|
|
109
|
+
agentIsActive: boolean
|
|
110
|
+
): StartupWorkItem[] {
|
|
111
|
+
const byStage = new Map(events.map((event) => [event.stage, event]));
|
|
112
|
+
const firstResponse = byStage.get('first_response');
|
|
113
|
+
|
|
114
|
+
return STARTUP_WORK_STAGES.flatMap((stage, stageIndex) => {
|
|
115
|
+
const started = byStage.get(stage.started);
|
|
116
|
+
const explicitlyCompleted = byStage.get(stage.completed);
|
|
117
|
+
if (!started && !explicitlyCompleted) return [];
|
|
118
|
+
|
|
119
|
+
const nextStageStarted = STARTUP_WORK_STAGES
|
|
120
|
+
.slice(stageIndex + 1)
|
|
121
|
+
.map((nextStage) => byStage.get(nextStage.started) || byStage.get(nextStage.completed))
|
|
122
|
+
.find((event): event is StartupProgressEvent => Boolean(event));
|
|
123
|
+
const inferredCompletion = stage.started === 'environment'
|
|
124
|
+
? firstResponse
|
|
125
|
+
: nextStageStarted;
|
|
126
|
+
const completed = explicitlyCompleted || inferredCompletion;
|
|
127
|
+
const isActive = !completed && !executionHasOutput && agentIsActive;
|
|
128
|
+
const startTimestamp = started?.timestamp || explicitlyCompleted?.timestamp || nowSeconds;
|
|
129
|
+
const endTimestamp = completed?.timestamp
|
|
130
|
+
|| (isActive ? nowSeconds : events[events.length - 1]?.timestamp || startTimestamp);
|
|
131
|
+
|
|
132
|
+
const liveDetail = (stage.liveDetailStages || [])
|
|
133
|
+
.map((detailStage) => byStage.get(detailStage))
|
|
134
|
+
.filter((event): event is StartupProgressEvent => Boolean(event))
|
|
135
|
+
.filter((event) => event.timestamp >= startTimestamp)
|
|
136
|
+
.sort((a, b) => b.timestamp - a.timestamp)[0];
|
|
137
|
+
const completedDetail = stage.completedDetailStage
|
|
138
|
+
? byStage.get(stage.completedDetailStage)
|
|
139
|
+
: undefined;
|
|
140
|
+
const message = completed
|
|
141
|
+
? (completedDetail?.message || explicitlyCompleted?.message || stage.completedMessage)
|
|
142
|
+
: (liveDetail?.message || started!.message);
|
|
143
|
+
|
|
144
|
+
return [{
|
|
145
|
+
...(explicitlyCompleted || started!),
|
|
146
|
+
message,
|
|
147
|
+
isActive,
|
|
148
|
+
elapsed: formatStartupElapsed(endTimestamp - startTimestamp)
|
|
149
|
+
}];
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
import {
|
|
154
|
+
parseNote,
|
|
155
|
+
parseNoteLines,
|
|
156
|
+
extractCheckerResult,
|
|
157
|
+
renderMarkdownToHtml
|
|
158
|
+
} from '../../utils/markdown-parser.util';
|
|
159
|
+
|
|
160
|
+
import {
|
|
161
|
+
getActionObject,
|
|
162
|
+
isAndroidAction,
|
|
163
|
+
getActionIcon,
|
|
164
|
+
getActionTitle,
|
|
165
|
+
getActionTargetText,
|
|
166
|
+
getActionInputLabel,
|
|
167
|
+
getActionInputText,
|
|
168
|
+
getActionCoords,
|
|
169
|
+
getActionBounds,
|
|
170
|
+
getActionResourceId,
|
|
171
|
+
getActionClass,
|
|
172
|
+
isActionFailed,
|
|
173
|
+
getActionErrorMessage,
|
|
174
|
+
extractActionExtraParams,
|
|
175
|
+
getStepPreImageUrl,
|
|
176
|
+
getStepPostImageUrl,
|
|
177
|
+
isReportStatusAction,
|
|
178
|
+
getReportStatusValue,
|
|
179
|
+
getReportStatusExplanation
|
|
180
|
+
} from '../../utils/action-formatter.util';
|
|
181
|
+
|
|
182
|
+
import {
|
|
183
|
+
getToolArgs,
|
|
184
|
+
isNoteTool,
|
|
185
|
+
isVideoTool,
|
|
186
|
+
getVideoAnalysisView,
|
|
187
|
+
formatVideoTime,
|
|
188
|
+
getVideoToolTarget,
|
|
189
|
+
isDeviceActionTool,
|
|
190
|
+
isFailureAnalyzerActionTool,
|
|
191
|
+
getToolAgentName,
|
|
192
|
+
shouldShowTool,
|
|
193
|
+
getUniqueGenericTools,
|
|
194
|
+
getToolKey,
|
|
195
|
+
getToolDisplayLabel,
|
|
196
|
+
isCompressionTool,
|
|
197
|
+
isCompressionWaiting,
|
|
198
|
+
getCompressionPhaseLabel,
|
|
199
|
+
getToolIcon,
|
|
200
|
+
getToolTitle,
|
|
201
|
+
getToolTargetText,
|
|
202
|
+
getToolInputLabel,
|
|
203
|
+
getToolInputText,
|
|
204
|
+
getToolCoords,
|
|
205
|
+
getToolAnalysisText,
|
|
206
|
+
getToolGenericDetails,
|
|
207
|
+
isAdbCommandTool,
|
|
208
|
+
getAdbCommandLine,
|
|
209
|
+
getAdbCwd,
|
|
210
|
+
getAdbTerminalId,
|
|
211
|
+
isToolFailed,
|
|
212
|
+
getToolErrorMessage,
|
|
213
|
+
extractToolExtraParams,
|
|
214
|
+
isHumanThinking,
|
|
215
|
+
cleanErrorMessage
|
|
216
|
+
} from '../../utils/tool-formatter.util';
|
|
217
|
+
|
|
218
|
+
import { drawActionCoordinatesOnOverlay } from '../../utils/image-overlay.util';
|
|
219
|
+
|
|
220
|
+
import {
|
|
221
|
+
consolidateLogsToBlocks,
|
|
222
|
+
isBackendSwitchNote,
|
|
223
|
+
groupBlocksToPhases,
|
|
224
|
+
extractBlockTokens,
|
|
225
|
+
formatTokenCount,
|
|
226
|
+
getSortedStepEvents,
|
|
227
|
+
compileSessionSummary,
|
|
228
|
+
computeSessionDuration,
|
|
229
|
+
checkPlanningLoader
|
|
230
|
+
} from '../../utils/stream-aggregator.util';
|
|
231
|
+
|
|
232
|
+
export type { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote, StreamResetNotice };
|
|
233
|
+
|
|
234
|
+
@Component({
|
|
235
|
+
selector: 'app-agent-stream',
|
|
236
|
+
standalone: true,
|
|
237
|
+
imports: [TranslatePipe, CommonModule, FormsModule, OverlayModule],
|
|
238
|
+
templateUrl: './agent-stream.component.html',
|
|
239
|
+
styleUrl: './agent-stream.component.scss',
|
|
240
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
241
|
+
})
|
|
242
|
+
export class AgentStreamComponent implements AfterViewInit {
|
|
243
|
+
public agentService = inject(AgentService);
|
|
244
|
+
private http = inject(HttpClient);
|
|
245
|
+
private destroyRef = inject(DestroyRef);
|
|
246
|
+
private zone = inject(NgZone);
|
|
247
|
+
|
|
248
|
+
// Auto-scroll state tracking
|
|
249
|
+
public isUserAtBottom = true;
|
|
250
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
251
|
+
private streamLogsContainer: HTMLElement | null = null;
|
|
252
|
+
private autoScrollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
253
|
+
private autoScrollStreamBoxes = false;
|
|
254
|
+
private onContainerScroll = () => {
|
|
255
|
+
const container = this.streamLogsContainer;
|
|
256
|
+
if (!container) return;
|
|
257
|
+
const threshold = 150;
|
|
258
|
+
const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
|
|
259
|
+
this.isUserAtBottom = distanceToBottom <= threshold;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Dynamic Planning Loader Game-style Phrases
|
|
263
|
+
public currentPlanningText = signal<string>(PLANNING_LOADER_PHRASES[0]);
|
|
264
|
+
private planningIntervalId: any = null;
|
|
265
|
+
private currentPhraseIndex = 0;
|
|
266
|
+
|
|
267
|
+
// Top Nav Dropdown States
|
|
268
|
+
public isTaskDropdownOpen = signal<boolean>(false);
|
|
269
|
+
public isNotesDropdownOpen = signal<boolean>(false);
|
|
270
|
+
|
|
271
|
+
// Run info popover (opened from the Flash / Pro chip): elapsed time, token
|
|
272
|
+
// usage, executor context share and the Pro tuning the run started with.
|
|
273
|
+
public isRunInfoOpen = signal<boolean>(false);
|
|
274
|
+
public runUsage = signal<SessionUsage | null>(null);
|
|
275
|
+
private runInfoClock = signal<number>(Date.now());
|
|
276
|
+
|
|
277
|
+
public runInfoIsPro = computed(() => this.getModelClass(this.agentService.viewedModel()?.name) === 'is-pro');
|
|
278
|
+
|
|
279
|
+
public runInfoElapsed = computed(() => {
|
|
280
|
+
const session = this.agentService.currentSession();
|
|
281
|
+
const running = this.agentService.isCurrentSessionRunning();
|
|
282
|
+
return formatElapsed(sessionElapsedSeconds(session, running, this.runInfoClock()));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
public runInfoTuning = computed(() => {
|
|
286
|
+
const usage = this.runUsage();
|
|
287
|
+
if (usage?.run_tuning && (usage.run_tuning.verification_level || usage.run_tuning.explorer_mode)) {
|
|
288
|
+
return usage.run_tuning;
|
|
289
|
+
}
|
|
290
|
+
return parseRunTuning(this.agentService.currentSession()?.device_info);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
public runInfoContextPercent = computed(() => {
|
|
294
|
+
const usage = this.runUsage();
|
|
295
|
+
if (!usage) return null;
|
|
296
|
+
return contextPercent(usage.operator_context_tokens, usage.operator_context_window_tokens);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// CDK Connected Overlay Positioning Strategy
|
|
300
|
+
public readonly dropdownPositions: ConnectedPosition[] = [
|
|
301
|
+
{
|
|
302
|
+
originX: 'end',
|
|
303
|
+
originY: 'bottom',
|
|
304
|
+
overlayX: 'end',
|
|
305
|
+
overlayY: 'top',
|
|
306
|
+
offsetY: 6,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
originX: 'end',
|
|
310
|
+
originY: 'top',
|
|
311
|
+
overlayX: 'end',
|
|
312
|
+
overlayY: 'bottom',
|
|
313
|
+
offsetY: -6,
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
originX: 'start',
|
|
317
|
+
originY: 'bottom',
|
|
318
|
+
overlayX: 'start',
|
|
319
|
+
overlayY: 'top',
|
|
320
|
+
offsetY: 6,
|
|
321
|
+
}
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
// Zoom Modal State
|
|
325
|
+
public selectedZoomImage = signal<string | null>(null);
|
|
326
|
+
public selectedZoomAction = signal<any | null>(null);
|
|
327
|
+
|
|
328
|
+
// Signals and properties for managing delayed collapsing of thought streams
|
|
329
|
+
public collapsedStreams = signal<Set<string>>(new Set<string>());
|
|
330
|
+
private scheduledCollapses = new Set<string>();
|
|
331
|
+
|
|
332
|
+
// Signals and maps for typewriter effect simulation. All active typewriters
|
|
333
|
+
// are driven by ONE requestAnimationFrame loop (run outside the Angular
|
|
334
|
+
// zone) that batches every text advance into a single signal update.
|
|
335
|
+
public typedTextsSignal = signal<Record<string, string>>({});
|
|
336
|
+
private typingTargets = new Map<string, {
|
|
337
|
+
blockId: string;
|
|
338
|
+
isThinking: boolean;
|
|
339
|
+
execId?: string;
|
|
340
|
+
fallbackText: string;
|
|
341
|
+
}>();
|
|
342
|
+
private typingRafId: number | null = null;
|
|
343
|
+
private lastTypingTimestamp = 0;
|
|
344
|
+
// ≈ legacy pace of 8 characters every 12ms interval tick.
|
|
345
|
+
private static readonly TYPING_CHARS_PER_SECOND = 667;
|
|
346
|
+
private static readonly TYPING_MIN_FRAME_MS = 28;
|
|
347
|
+
|
|
348
|
+
// Active reset notices per block (displayed after typewriter rewind finishes)
|
|
349
|
+
public activeBlockResets = signal<Map<string, StreamResetNotice[]>>(new Map());
|
|
350
|
+
public activeResetNotices = signal<Map<string, string>>(new Map());
|
|
351
|
+
private rewindTargets = new Map<string, {
|
|
352
|
+
blockId: string;
|
|
353
|
+
isThinking: boolean;
|
|
354
|
+
execId?: string;
|
|
355
|
+
message: string;
|
|
356
|
+
}>();
|
|
357
|
+
// Fast rewind pace (backspace speed ≈ 4x normal typing speed for snappy deletion)
|
|
358
|
+
private static readonly REWIND_CHARS_PER_SECOND = 2600;
|
|
359
|
+
|
|
360
|
+
// Rendered-markdown memo per template slot: returning the identical string
|
|
361
|
+
// instance for unchanged text lets the [innerHTML] binding skip re-sanitizing
|
|
362
|
+
// and re-parsing the whole fragment on every change-detection pass.
|
|
363
|
+
private markdownHtmlCache = new Map<string, { source: string; html: string }>();
|
|
364
|
+
// Thinking-text extraction memo (includes the JSON-detection heuristics).
|
|
365
|
+
private thinkingTextCache = new WeakMap<any, { native: string | null; raw: string | null }>();
|
|
366
|
+
private deviceSerialCache = new WeakMap<Session, string | null>();
|
|
367
|
+
|
|
368
|
+
// Set to track expanded and collapsed state of action cards
|
|
369
|
+
public expandedActionCards = signal<Set<string>>(new Set<string>());
|
|
370
|
+
public collapsedActionCards = signal<Set<string>>(new Set<string>());
|
|
371
|
+
public retryClock = signal<number>(Date.now());
|
|
372
|
+
|
|
373
|
+
// Performance caches for parameter and event extractions
|
|
374
|
+
private actionParamsCache = new WeakMap<any, ActionParam[]>();
|
|
375
|
+
private toolParamsCache = new WeakMap<any, ActionParam[]>();
|
|
376
|
+
private sortedEventsCache = new WeakMap<any, { signature: string; events: StepEvent[] }>();
|
|
377
|
+
|
|
378
|
+
// Top Nav Task Queue Computed Properties
|
|
379
|
+
public activeQueue = computed(() => {
|
|
380
|
+
const list = this.agentService.sessions().filter((s) => {
|
|
381
|
+
const status = this.getTaskStatus(s);
|
|
382
|
+
return status === 'running' || status === 'paused' || status === 'pending';
|
|
383
|
+
});
|
|
384
|
+
return list.sort((a, b) => {
|
|
385
|
+
const statusA = this.getTaskStatus(a);
|
|
386
|
+
const statusB = this.getTaskStatus(b);
|
|
387
|
+
const isRunA = statusA === 'running' || statusA === 'paused';
|
|
388
|
+
const isRunB = statusB === 'running' || statusB === 'paused';
|
|
389
|
+
if (isRunA && !isRunB) return -1;
|
|
390
|
+
if (!isRunA && isRunB) return 1;
|
|
391
|
+
return (a.start_time || 0) - (b.start_time || 0);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
public historyTasks = computed(() => {
|
|
396
|
+
return this.agentService.sessions().filter((s) => {
|
|
397
|
+
const status = this.getTaskStatus(s);
|
|
398
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Top Nav Notes Computed Properties
|
|
403
|
+
public currentNoteContent = computed(() => {
|
|
404
|
+
const notes = this.agentService.currentNotes();
|
|
405
|
+
const key = this.agentService.selectedNoteKey();
|
|
406
|
+
return notes[key] || '';
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
public noteKeys = computed(() => {
|
|
410
|
+
return Object.keys(this.agentService.currentNotes()).filter(key => key.toLowerCase().endsWith('.md'));
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
public outputterReport = computed(() => {
|
|
415
|
+
const notes = this.agentService.currentNotes();
|
|
416
|
+
return notes['output.md'] || null;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Memoized so the Task Report card is not re-parsed (and its DOM rebuilt)
|
|
420
|
+
// on every change-detection pass while streams are typing.
|
|
421
|
+
public parsedOutputReport = computed<ParsedNote | null>(() => {
|
|
422
|
+
const report = this.outputterReport();
|
|
423
|
+
return report ? parseNote(report) : null;
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
public parsedNote = computed<ParsedNote>(() => {
|
|
427
|
+
return this.getParsedNote(this.currentNoteContent());
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Stream Log Filtering & Aggregation Computed Properties
|
|
431
|
+
public filteredLogs = computed(() => {
|
|
432
|
+
const logs = this.agentService.sessionLogs();
|
|
433
|
+
const currentSessionId = this.agentService.currentSessionId();
|
|
434
|
+
if (!currentSessionId) return logs;
|
|
435
|
+
return logs.filter((log: any) => {
|
|
436
|
+
const logSessionId = log.session_id || log.data?.session_id;
|
|
437
|
+
return !logSessionId || logSessionId === currentSessionId;
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
public sessionSummary = computed(() => {
|
|
442
|
+
return compileSessionSummary(this.filteredLogs());
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
public showPlanningLoader = computed(() => {
|
|
446
|
+
const isRunning = this.agentService.agentStatus() === 'running';
|
|
447
|
+
const currentSessionId = this.agentService.currentSessionId();
|
|
448
|
+
const runningSessionId = this.agentService.runningSessionId();
|
|
449
|
+
const isCurrentRunningSession = !currentSessionId || currentSessionId === runningSessionId;
|
|
450
|
+
return checkPlanningLoader(this.filteredLogs(), isRunning, isCurrentRunningSession);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
public isViewingPausedTask = computed(() => {
|
|
454
|
+
// A live task_paused event and the polled runner status must agree before
|
|
455
|
+
// recovery controls are shown. This prevents a stale failure trace from
|
|
456
|
+
// offering Resume while the active task is still reported as running.
|
|
457
|
+
if (!this.agentService.isPaused() || this.agentService.agentStatus() !== 'paused') return false;
|
|
458
|
+
|
|
459
|
+
const currentSessionId = this.agentService.currentSessionId();
|
|
460
|
+
const pausedSessionId = this.agentService.runningSessionId();
|
|
461
|
+
return !currentSessionId || currentSessionId === pausedSessionId;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
public sessionDuration = computed(() => {
|
|
465
|
+
const currentSession = this.agentService.sessions().find(s => s.session_id === this.agentService.currentSessionId());
|
|
466
|
+
return computeSessionDuration(currentSession?.start_time || 0, this.filteredLogs());
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
public consolidatedBlocks = computed<StepBlock[]>(() => {
|
|
470
|
+
return consolidateLogsToBlocks(this.filteredLogs());
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
public phases = computed<PhaseBlock[]>(() => {
|
|
474
|
+
const currentSession = this.agentService.sessions().find(s => s.session_id === this.agentService.currentSessionId());
|
|
475
|
+
return groupBlocksToPhases(this.consolidatedBlocks(), currentSession?.start_time || 0);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
public startupWorkItems = computed(() => {
|
|
479
|
+
this.retryClock();
|
|
480
|
+
const events = this.agentService.currentStartupProgress();
|
|
481
|
+
const agentIsActive = ['running', 'paused'].includes(this.agentService.agentStatus());
|
|
482
|
+
return buildStartupWorkItems(
|
|
483
|
+
events,
|
|
484
|
+
Date.now() / 1000,
|
|
485
|
+
this.consolidatedBlocks().length > 0,
|
|
486
|
+
agentIsActive
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
public startupPreparationIsRunning = computed(() => {
|
|
491
|
+
return this.startupWorkItems().some((item) => item.isActive);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
public startupPreparationIsComplete = computed(() => {
|
|
495
|
+
const items = this.startupWorkItems();
|
|
496
|
+
return items.length > 0 && !items.some((item) => item.isActive);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
public startupPreparationDuration = computed(() => {
|
|
500
|
+
const items = this.startupWorkItems();
|
|
501
|
+
if (items.length === 0) return 0;
|
|
502
|
+
|
|
503
|
+
const events = this.agentService.currentStartupProgress();
|
|
504
|
+
const startedAt = events[0]?.timestamp || items[0].timestamp;
|
|
505
|
+
const completedAt = events.find((event) => event.stage === 'environment_ready')?.timestamp
|
|
506
|
+
|| events.find((event) => event.stage === 'first_response')?.timestamp;
|
|
507
|
+
const endedAt = this.startupPreparationIsRunning()
|
|
508
|
+
? Date.now() / 1000
|
|
509
|
+
: completedAt || Math.max(...items.map((item) => item.timestamp));
|
|
510
|
+
|
|
511
|
+
return Math.max(1, Math.round(endedAt - startedAt));
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
private retryablePauseTrace = computed<any | null>(() => {
|
|
515
|
+
if (!this.isViewingPausedTask()) return null;
|
|
516
|
+
|
|
517
|
+
const blocks = this.consolidatedBlocks();
|
|
518
|
+
for (let blockIndex = blocks.length - 1; blockIndex >= 0; blockIndex--) {
|
|
519
|
+
const tools = blocks[blockIndex]?.data?.generic_tools;
|
|
520
|
+
if (!Array.isArray(tools)) continue;
|
|
521
|
+
|
|
522
|
+
for (let toolIndex = tools.length - 1; toolIndex >= 0; toolIndex--) {
|
|
523
|
+
const tool = tools[toolIndex];
|
|
524
|
+
// Older persisted traces used the llm_pause name without adding the
|
|
525
|
+
// newer payload.pause marker. Treat both contracts consistently.
|
|
526
|
+
if (this.isDisplayableLLMFailure(tool)) {
|
|
527
|
+
return tool;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
public hasVisibleBlocks(phase: any): boolean {
|
|
535
|
+
if (!phase || !phase.blocks) return false;
|
|
536
|
+
return phase.blocks.some((b: any) => this.hasVisibleContent(b));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
constructor() {
|
|
540
|
+
// Run info popover: fetch usage when opened or when the viewed session
|
|
541
|
+
// changes, and keep refreshing while that session is still running.
|
|
542
|
+
effect((onCleanup) => {
|
|
543
|
+
const open = this.isRunInfoOpen();
|
|
544
|
+
const sessionId = this.agentService.currentSessionId();
|
|
545
|
+
const running = this.agentService.isCurrentSessionRunning();
|
|
546
|
+
if (!open || !sessionId) return;
|
|
547
|
+
if (untracked(() => this.runUsage())?.session_id !== sessionId) {
|
|
548
|
+
this.runUsage.set(null);
|
|
549
|
+
}
|
|
550
|
+
this.loadRunUsage(sessionId);
|
|
551
|
+
if (!running) return;
|
|
552
|
+
const usageInterval = setInterval(() => this.loadRunUsage(sessionId), 3000);
|
|
553
|
+
onCleanup(() => clearInterval(usageInterval));
|
|
554
|
+
});
|
|
555
|
+
effect((onCleanup) => {
|
|
556
|
+
if (!this.isRunInfoOpen() || !this.agentService.isCurrentSessionRunning()) return;
|
|
557
|
+
this.runInfoClock.set(Date.now());
|
|
558
|
+
const clockInterval = setInterval(() => this.runInfoClock.set(Date.now()), 1000);
|
|
559
|
+
onCleanup(() => clearInterval(clockInterval));
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// The retry clock only needs to tick while something on screen is timing
|
|
563
|
+
// (startup progress, LLM retry countdowns). An idle page stays quiet.
|
|
564
|
+
this.zone.runOutsideAngular(() => {
|
|
565
|
+
const retryClockInterval = setInterval(() => {
|
|
566
|
+
const status = this.agentService.agentStatus();
|
|
567
|
+
if (status === 'running' || status === 'paused' || this.agentService.isRetrying()) {
|
|
568
|
+
this.retryClock.set(Date.now());
|
|
569
|
+
}
|
|
570
|
+
}, 1000);
|
|
571
|
+
this.destroyRef.onDestroy(() => clearInterval(retryClockInterval));
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Auto scroll stream box during active text streaming
|
|
575
|
+
effect(() => {
|
|
576
|
+
const logs = this.filteredLogs();
|
|
577
|
+
const activeStream = logs.find(log => log.type === 'llm_stream' && !log.data?.isCompleted);
|
|
578
|
+
if (activeStream) {
|
|
579
|
+
this.scheduleAutoScroll(true);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Auto scroll main log list when new logs are added or typewriter updates
|
|
584
|
+
effect(() => {
|
|
585
|
+
this.consolidatedBlocks();
|
|
586
|
+
this.typedTextsSignal();
|
|
587
|
+
this.scheduleAutoScroll(false);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// Listen for stream reset events to trigger fast typewriter rewind
|
|
591
|
+
effect(() => {
|
|
592
|
+
const resetEvent = this.agentService.streamResetEvent();
|
|
593
|
+
if (!resetEvent) return;
|
|
594
|
+
untracked(() => {
|
|
595
|
+
this.triggerStreamRewind(
|
|
596
|
+
resetEvent.stream_execution_id || resetEvent.stream_exec_id,
|
|
597
|
+
resetEvent.message || DEFAULT_STREAM_RESET_MESSAGE
|
|
598
|
+
);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Typewriter drive logic: triggers typing when block appears or expands
|
|
603
|
+
effect(() => {
|
|
604
|
+
const blocks = this.consolidatedBlocks();
|
|
605
|
+
blocks.forEach(block => {
|
|
606
|
+
// Check blocks render their per-turn segments directly (no typewriter).
|
|
607
|
+
if (block.type === 'checker') return;
|
|
608
|
+
const blockId = block.id;
|
|
609
|
+
|
|
610
|
+
// If the block is flagged as reset, ensure typewriter rewind is active or reset notice is set
|
|
611
|
+
if (block.data?.isReset) {
|
|
612
|
+
untracked(() => {
|
|
613
|
+
const resetMsg = block.data.resetMessage || DEFAULT_STREAM_RESET_MESSAGE;
|
|
614
|
+
const currentRecord = this.typedTextsSignal();
|
|
615
|
+
const hasText = (currentRecord[blockId]?.length || 0) > 0 || (currentRecord[blockId + '-native']?.length || 0) > 0;
|
|
616
|
+
if (hasText) {
|
|
617
|
+
if (!this.rewindTargets.has(blockId) && !this.rewindTargets.has(blockId + '-native')) {
|
|
618
|
+
this.triggerStreamRewind(block.data.execution_id, resetMsg);
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
const activeResets = this.activeBlockResets().get(blockId) || [];
|
|
622
|
+
if (activeResets.length === 0) {
|
|
623
|
+
this.upsertBlockResetNotice(blockId, {
|
|
624
|
+
id: block.data.execution_id || `${blockId}-text`,
|
|
625
|
+
message: resetMsg,
|
|
626
|
+
isWaiting: !block.data?.isCompleted,
|
|
627
|
+
streamType: 'text'
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
if (!this.activeResetNotices().has(blockId)) {
|
|
631
|
+
this.activeResetNotices.update(m => {
|
|
632
|
+
const next = new Map(m);
|
|
633
|
+
next.set(blockId, resetMsg);
|
|
634
|
+
return next;
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const rawText = this.getRawThinking(block) || '';
|
|
643
|
+
const nativeText = this.getNativeThinking(block) || '';
|
|
644
|
+
|
|
645
|
+
untracked(() => {
|
|
646
|
+
// Handle Raw Thinking (Work)
|
|
647
|
+
const currentRecord = this.typedTextsSignal();
|
|
648
|
+
if (rawText) {
|
|
649
|
+
if (!(blockId in currentRecord)) {
|
|
650
|
+
if (block.data?.isCompleted) {
|
|
651
|
+
this.typedTextsSignal.update(r => ({ ...r, [blockId]: rawText }));
|
|
652
|
+
} else {
|
|
653
|
+
this.typedTextsSignal.update(r => ({ ...r, [blockId]: '' }));
|
|
654
|
+
this.startTyping(blockId, rawText, false);
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
const currentVal = currentRecord[blockId] || '';
|
|
658
|
+
if (currentVal.length < rawText.length && !this.typingTargets.has(blockId)) {
|
|
659
|
+
if (block.data?.isCompleted) {
|
|
660
|
+
this.typedTextsSignal.update(r => ({ ...r, [blockId]: rawText }));
|
|
661
|
+
} else {
|
|
662
|
+
this.startTyping(blockId, rawText, false);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Handle Native Thinking (Thought)
|
|
669
|
+
const nativeKey = blockId + '-native';
|
|
670
|
+
if (nativeText) {
|
|
671
|
+
const execId = block.data?.execution_id || block.data?.step_id || blockId;
|
|
672
|
+
if (!(nativeKey in currentRecord)) {
|
|
673
|
+
if (block.data?.isCompleted) {
|
|
674
|
+
this.typedTextsSignal.update(r => ({ ...r, [nativeKey]: nativeText }));
|
|
675
|
+
this.collapsedStreams.update(set => {
|
|
676
|
+
const newSet = new Set(set);
|
|
677
|
+
newSet.add(execId);
|
|
678
|
+
return newSet;
|
|
679
|
+
});
|
|
680
|
+
} else {
|
|
681
|
+
this.typedTextsSignal.update(r => ({ ...r, [nativeKey]: '' }));
|
|
682
|
+
this.startTyping(nativeKey, nativeText, true, execId);
|
|
683
|
+
}
|
|
684
|
+
} else {
|
|
685
|
+
const currentVal = currentRecord[nativeKey] || '';
|
|
686
|
+
if (currentVal.length < nativeText.length && !this.typingTargets.has(nativeKey)) {
|
|
687
|
+
if (block.data?.isCompleted) {
|
|
688
|
+
this.typedTextsSignal.update(r => ({ ...r, [nativeKey]: nativeText }));
|
|
689
|
+
this.collapsedStreams.update(set => {
|
|
690
|
+
const newSet = new Set(set);
|
|
691
|
+
newSet.add(execId);
|
|
692
|
+
return newSet;
|
|
693
|
+
});
|
|
694
|
+
} else {
|
|
695
|
+
this.startTyping(nativeKey, nativeText, true, execId);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// Reset typewriter and collapse states when session changes
|
|
705
|
+
effect(() => {
|
|
706
|
+
this.agentService.currentSessionId();
|
|
707
|
+
untracked(() => {
|
|
708
|
+
this.stopTypingLoop();
|
|
709
|
+
this.typedTextsSignal.set({});
|
|
710
|
+
this.activeResetNotices.set(new Map());
|
|
711
|
+
this.activeBlockResets.set(new Map());
|
|
712
|
+
this.markdownHtmlCache.clear();
|
|
713
|
+
this.scheduledCollapses.clear();
|
|
714
|
+
this.collapsedStreams.set(new Set<string>());
|
|
715
|
+
this.isUserAtBottom = true;
|
|
716
|
+
|
|
717
|
+
setTimeout(() => {
|
|
718
|
+
const container = document.querySelector('.stream-logs-content');
|
|
719
|
+
if (container) {
|
|
720
|
+
container.scrollTop = container.scrollHeight;
|
|
721
|
+
}
|
|
722
|
+
}, 100);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Periodically rotate loading text like game loading screen hints
|
|
727
|
+
effect(() => {
|
|
728
|
+
const isLoading = this.showPlanningLoader();
|
|
729
|
+
if (isLoading) {
|
|
730
|
+
if (!this.planningIntervalId) {
|
|
731
|
+
this.currentPhraseIndex = Math.floor(Math.random() * PLANNING_LOADER_PHRASES.length);
|
|
732
|
+
this.currentPlanningText.set(PLANNING_LOADER_PHRASES[this.currentPhraseIndex]);
|
|
733
|
+
|
|
734
|
+
this.planningIntervalId = setInterval(() => {
|
|
735
|
+
this.currentPhraseIndex = (this.currentPhraseIndex + 1) % PLANNING_LOADER_PHRASES.length;
|
|
736
|
+
this.currentPlanningText.set(PLANNING_LOADER_PHRASES[this.currentPhraseIndex]);
|
|
737
|
+
}, 2800);
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
if (this.planningIntervalId) {
|
|
741
|
+
clearInterval(this.planningIntervalId);
|
|
742
|
+
this.planningIntervalId = null;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
this.destroyRef.onDestroy(() => {
|
|
748
|
+
if (this.planningIntervalId) {
|
|
749
|
+
clearInterval(this.planningIntervalId);
|
|
750
|
+
this.planningIntervalId = null;
|
|
751
|
+
}
|
|
752
|
+
if (this.resizeObserver) {
|
|
753
|
+
this.resizeObserver.disconnect();
|
|
754
|
+
this.resizeObserver = null;
|
|
755
|
+
}
|
|
756
|
+
this.stopTypingLoop();
|
|
757
|
+
if (this.autoScrollTimer) {
|
|
758
|
+
clearTimeout(this.autoScrollTimer);
|
|
759
|
+
this.autoScrollTimer = null;
|
|
760
|
+
}
|
|
761
|
+
if (this.streamLogsContainer) {
|
|
762
|
+
this.streamLogsContainer.removeEventListener('scroll', this.onContainerScroll);
|
|
763
|
+
this.streamLogsContainer = null;
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
public ngAfterViewInit(): void {
|
|
769
|
+
const container = document.querySelector('.stream-logs-content') as HTMLElement | null;
|
|
770
|
+
this.streamLogsContainer = container;
|
|
771
|
+
if (!container) return;
|
|
772
|
+
|
|
773
|
+
// The scroll listener lives outside the Angular zone: tracking the
|
|
774
|
+
// user's scroll position must not trigger change detection per frame.
|
|
775
|
+
this.zone.runOutsideAngular(() => {
|
|
776
|
+
container.addEventListener('scroll', this.onContainerScroll, { passive: true });
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
780
|
+
this.resizeObserver = new ResizeObserver(() => {
|
|
781
|
+
if (this.isUserAtBottom) {
|
|
782
|
+
this.scheduleAutoScroll(false);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
this.resizeObserver.observe(container);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Coalesce every auto-scroll request into one deferred pass that batches all
|
|
791
|
+
* DOM reads before all writes, instead of thrashing layout per update.
|
|
792
|
+
*/
|
|
793
|
+
private scheduleAutoScroll(includeStreamBoxes: boolean): void {
|
|
794
|
+
this.autoScrollStreamBoxes = this.autoScrollStreamBoxes || includeStreamBoxes;
|
|
795
|
+
if (this.autoScrollTimer) return;
|
|
796
|
+
this.zone.runOutsideAngular(() => {
|
|
797
|
+
this.autoScrollTimer = setTimeout(() => {
|
|
798
|
+
this.autoScrollTimer = null;
|
|
799
|
+
const includeBoxes = this.autoScrollStreamBoxes;
|
|
800
|
+
this.autoScrollStreamBoxes = false;
|
|
801
|
+
|
|
802
|
+
const boxes = includeBoxes
|
|
803
|
+
? (Array.from(document.getElementsByClassName('stream-box')) as HTMLElement[])
|
|
804
|
+
: [];
|
|
805
|
+
const boxTargets = boxes.map((el) => el.scrollHeight);
|
|
806
|
+
const container = this.streamLogsContainer;
|
|
807
|
+
const containerTarget = container && this.isUserAtBottom ? container.scrollHeight : null;
|
|
808
|
+
|
|
809
|
+
boxes.forEach((el, i) => { el.scrollTop = boxTargets[i]; });
|
|
810
|
+
if (container && containerTarget !== null) {
|
|
811
|
+
container.scrollTop = containerTarget;
|
|
812
|
+
}
|
|
813
|
+
}, 50);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Top Nav Action Methods
|
|
818
|
+
public toggleTaskDropdown(event: Event): void {
|
|
819
|
+
event.stopPropagation();
|
|
820
|
+
this.isTaskDropdownOpen.update(v => !v);
|
|
821
|
+
if (this.isTaskDropdownOpen()) {
|
|
822
|
+
this.isNotesDropdownOpen.set(false);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
public toggleNotesDropdown(event: Event): void {
|
|
827
|
+
event.stopPropagation();
|
|
828
|
+
this.isNotesDropdownOpen.update((v) => !v);
|
|
829
|
+
if (this.isNotesDropdownOpen()) {
|
|
830
|
+
this.isTaskDropdownOpen.set(false);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
public openOutputterNote(event?: Event): void {
|
|
835
|
+
if (event) event.stopPropagation();
|
|
836
|
+
this.agentService.selectedNoteKey.set('output.md');
|
|
837
|
+
this.isNotesDropdownOpen.set(true);
|
|
838
|
+
this.isTaskDropdownOpen.set(false);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
public closeAllDropdowns(): void {
|
|
842
|
+
this.isTaskDropdownOpen.set(false);
|
|
843
|
+
this.isNotesDropdownOpen.set(false);
|
|
844
|
+
this.isRunInfoOpen.set(false);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
public toggleRunInfo(event: Event): void {
|
|
848
|
+
event.stopPropagation();
|
|
849
|
+
this.isRunInfoOpen.update((v) => !v);
|
|
850
|
+
if (this.isRunInfoOpen()) {
|
|
851
|
+
this.isTaskDropdownOpen.set(false);
|
|
852
|
+
this.isNotesDropdownOpen.set(false);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
private loadRunUsage(sessionId: string): void {
|
|
857
|
+
this.agentService.getSessionUsage(sessionId).subscribe({
|
|
858
|
+
next: (usage) => {
|
|
859
|
+
if (this.agentService.currentSessionId() !== sessionId) return;
|
|
860
|
+
this.runUsage.set(usage);
|
|
861
|
+
},
|
|
862
|
+
error: (err) => console.error('Failed to load session usage:', err)
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
public formatRunElapsed(seconds: number): string {
|
|
867
|
+
return formatElapsed(seconds);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
public formatRunTokens(tokens: number | null | undefined): string {
|
|
871
|
+
return formatCompactTokens(tokens);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
public runTuningLabel(kind: 'verify' | 'explore', id: string | null | undefined): string {
|
|
875
|
+
return tuningLabel(kind, id);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
public getTaskStatus(session: Session): 'running' | 'paused' | 'completed' | 'pending' | 'failed' | 'cancelled' {
|
|
879
|
+
if (session.status) {
|
|
880
|
+
const s = session.status.toLowerCase();
|
|
881
|
+
if (s === 'completed' || s === 'success' || s === 'failed' || s === 'cancelled') {
|
|
882
|
+
return (s === 'success' ? 'completed' : s) as any;
|
|
883
|
+
}
|
|
884
|
+
if (s === 'running' || s === 'paused' || s === 'pending') {
|
|
885
|
+
return s as any;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (session.session_id === this.agentService.runningSessionId() && (this.agentService.agentStatus() === 'running' || this.agentService.agentStatus() === 'paused')) {
|
|
889
|
+
return this.agentService.agentStatus() as 'running' | 'paused';
|
|
890
|
+
}
|
|
891
|
+
return 'completed';
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Determine the device serial number for the session
|
|
896
|
+
*/
|
|
897
|
+
public getDeviceSerial(session: Session): string | null {
|
|
898
|
+
if (this.deviceSerialCache.has(session)) {
|
|
899
|
+
return this.deviceSerialCache.get(session) ?? null;
|
|
900
|
+
}
|
|
901
|
+
let resolved: string | null = null;
|
|
902
|
+
const serial = session.device_serial || session.device_id;
|
|
903
|
+
if (serial && serial !== 'pending' && serial !== 'null' && serial !== 'undefined') {
|
|
904
|
+
resolved = serial;
|
|
905
|
+
} else if (session.device_info) {
|
|
906
|
+
try {
|
|
907
|
+
const info = typeof session.device_info === 'string' ? JSON.parse(session.device_info) : session.device_info;
|
|
908
|
+
const s = info?.device_id || info?.device_serial;
|
|
909
|
+
if (s && s !== 'pending' && s !== 'null' && s !== 'undefined') {
|
|
910
|
+
resolved = s;
|
|
911
|
+
}
|
|
912
|
+
} catch {
|
|
913
|
+
// ignore
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
this.deviceSerialCache.set(session, resolved);
|
|
917
|
+
return resolved;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
public selectTask(sessionId: string, event?: Event): void {
|
|
921
|
+
if (event) event.stopPropagation();
|
|
922
|
+
this.agentService.selectSession(sessionId, true);
|
|
923
|
+
this.isTaskDropdownOpen.set(false);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
public stopTask(sessionId: string, event?: Event): void {
|
|
927
|
+
if (event) event.stopPropagation();
|
|
928
|
+
this.agentService.stopTask(sessionId, false);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
public deleteTask(sessionId: string, event?: Event): void {
|
|
932
|
+
if (event) event.stopPropagation();
|
|
933
|
+
if (!confirm(`Are you sure you want to delete this task? This cannot be undone.`)) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
this.agentService.deleteSession(sessionId).subscribe({
|
|
937
|
+
error: (err) => {
|
|
938
|
+
console.error(`Failed to delete session ${sessionId}:`, err);
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
public selectNote(key: string, event?: Event): void {
|
|
944
|
+
if (event) event.stopPropagation();
|
|
945
|
+
this.agentService.selectedNoteKey.set(key);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
public getParsedNote(content: string): ParsedNote {
|
|
949
|
+
return parseNote(content);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
public getParsedNoteLines(content: string): MarkdownLine[] {
|
|
953
|
+
return parseNoteLines(content);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Typewriter & Delayed Collapse
|
|
957
|
+
private upsertBlockResetNotice(blockId: string, notice: StreamResetNotice): void {
|
|
958
|
+
this.activeBlockResets.update(m => {
|
|
959
|
+
const next = new Map(m);
|
|
960
|
+
const list = [...(next.get(blockId) || [])];
|
|
961
|
+
const existingIdx = list.findIndex(r => r.id === notice.id);
|
|
962
|
+
if (existingIdx > -1) {
|
|
963
|
+
list[existingIdx] = notice;
|
|
964
|
+
} else {
|
|
965
|
+
list.push(notice);
|
|
966
|
+
}
|
|
967
|
+
next.set(blockId, list);
|
|
968
|
+
return next;
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
private startTyping(key: string, targetText: string, isThinking: boolean = false, execId?: string) {
|
|
973
|
+
const blockId = key.endsWith('-native') ? key.replace('-native', '') : key;
|
|
974
|
+
// If there was an active rewind, cancel it, reset text to empty, and record its reset notice as completed waiting
|
|
975
|
+
const activeRewind = this.rewindTargets.get(key);
|
|
976
|
+
if (activeRewind) {
|
|
977
|
+
this.rewindTargets.delete(key);
|
|
978
|
+
this.typedTextsSignal.update(r => ({ ...r, [key]: '' }));
|
|
979
|
+
this.upsertBlockResetNotice(blockId, {
|
|
980
|
+
id: activeRewind.execId || `${blockId}-${isThinking ? 'thinking' : 'text'}`,
|
|
981
|
+
message: activeRewind.message,
|
|
982
|
+
isWaiting: false,
|
|
983
|
+
streamType: isThinking ? 'thinking' : 'text'
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Cancel any active rewind on this key
|
|
988
|
+
this.rewindTargets.delete(key);
|
|
989
|
+
|
|
990
|
+
// When retry stream starts outputting content, change waiting dot to normal dot (isWaiting: false)
|
|
991
|
+
// The previous prompt sentence DOES NOT disappear.
|
|
992
|
+
this.activeBlockResets.update(m => {
|
|
993
|
+
const list = m.get(blockId);
|
|
994
|
+
if (!list || list.length === 0) return m;
|
|
995
|
+
const targetType = isThinking ? 'thinking' : 'text';
|
|
996
|
+
const updated = list.map(r => r.streamType === targetType ? { ...r, isWaiting: false } : r);
|
|
997
|
+
const next = new Map(m);
|
|
998
|
+
next.set(blockId, updated);
|
|
999
|
+
return next;
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
this.typingTargets.set(key, { blockId, isThinking, execId, fallbackText: targetText });
|
|
1003
|
+
this.ensureTypingLoop();
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private ensureTypingLoop(): void {
|
|
1007
|
+
if (this.typingRafId !== null) return;
|
|
1008
|
+
this.lastTypingTimestamp = performance.now();
|
|
1009
|
+
this.zone.runOutsideAngular(() => {
|
|
1010
|
+
const step = (now: number) => {
|
|
1011
|
+
this.typingRafId = null;
|
|
1012
|
+
const elapsed = now - this.lastTypingTimestamp;
|
|
1013
|
+
if (elapsed >= AgentStreamComponent.TYPING_MIN_FRAME_MS) {
|
|
1014
|
+
this.lastTypingTimestamp = now;
|
|
1015
|
+
this.advanceTyping(elapsed);
|
|
1016
|
+
}
|
|
1017
|
+
if (this.typingTargets.size > 0 || this.rewindTargets.size > 0) {
|
|
1018
|
+
this.typingRafId = requestAnimationFrame(step);
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
this.typingRafId = requestAnimationFrame(step);
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
private stopTypingLoop(): void {
|
|
1026
|
+
this.typingTargets.clear();
|
|
1027
|
+
this.rewindTargets.clear();
|
|
1028
|
+
if (this.typingRafId !== null) {
|
|
1029
|
+
cancelAnimationFrame(this.typingRafId);
|
|
1030
|
+
this.typingRafId = null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Advance every active typewriter by the elapsed wall time and publish all
|
|
1036
|
+
* changes as one signal update (one render pass per animation frame at most).
|
|
1037
|
+
*/
|
|
1038
|
+
private advanceTyping(elapsedMs: number): void {
|
|
1039
|
+
const blocks = untracked(() => this.consolidatedBlocks());
|
|
1040
|
+
const currentRecord = untracked(() => this.typedTextsSignal());
|
|
1041
|
+
const chars = Math.max(
|
|
1042
|
+
1,
|
|
1043
|
+
Math.round(AgentStreamComponent.TYPING_CHARS_PER_SECOND * elapsedMs / 1000)
|
|
1044
|
+
);
|
|
1045
|
+
const rewindChars = Math.max(
|
|
1046
|
+
2,
|
|
1047
|
+
Math.round(AgentStreamComponent.REWIND_CHARS_PER_SECOND * elapsedMs / 1000)
|
|
1048
|
+
);
|
|
1049
|
+
const updates: Record<string, string> = {};
|
|
1050
|
+
let hasUpdates = false;
|
|
1051
|
+
|
|
1052
|
+
// 1. Advance forward typing targets
|
|
1053
|
+
for (const [key, typing] of this.typingTargets) {
|
|
1054
|
+
const block = blocks.find((b) => b.id === typing.blockId);
|
|
1055
|
+
let target = typing.fallbackText;
|
|
1056
|
+
if (block) {
|
|
1057
|
+
target = (typing.isThinking ? this.getNativeThinking(block) : this.getRawThinking(block))
|
|
1058
|
+
|| typing.fallbackText;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const current = currentRecord[key] || '';
|
|
1062
|
+
if (current.length < target.length) {
|
|
1063
|
+
updates[key] = target.slice(0, Math.min(current.length + chars, target.length));
|
|
1064
|
+
hasUpdates = true;
|
|
1065
|
+
} else if (block?.data?.isCompleted) {
|
|
1066
|
+
this.typingTargets.delete(key);
|
|
1067
|
+
if (typing.isThinking && typing.execId) {
|
|
1068
|
+
this.triggerDelayedCollapse(typing.execId);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// 2. Advance fast rewind targets (backspacing for mid-stream resets)
|
|
1074
|
+
const completedRewinds: Array<{ blockId: string; execId?: string; isThinking: boolean; message: string }> = [];
|
|
1075
|
+
for (const [key, rewind] of this.rewindTargets) {
|
|
1076
|
+
const current = updates[key] !== undefined ? updates[key] : (currentRecord[key] || '');
|
|
1077
|
+
if (current.length > 0) {
|
|
1078
|
+
const nextLen = Math.max(0, current.length - rewindChars);
|
|
1079
|
+
updates[key] = current.slice(0, nextLen);
|
|
1080
|
+
hasUpdates = true;
|
|
1081
|
+
if (nextLen === 0) {
|
|
1082
|
+
this.rewindTargets.delete(key);
|
|
1083
|
+
completedRewinds.push({
|
|
1084
|
+
blockId: rewind.blockId,
|
|
1085
|
+
execId: rewind.execId,
|
|
1086
|
+
isThinking: rewind.isThinking,
|
|
1087
|
+
message: rewind.message
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
this.rewindTargets.delete(key);
|
|
1092
|
+
completedRewinds.push({
|
|
1093
|
+
blockId: rewind.blockId,
|
|
1094
|
+
execId: rewind.execId,
|
|
1095
|
+
isThinking: rewind.isThinking,
|
|
1096
|
+
message: rewind.message
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (completedRewinds.length > 0) {
|
|
1102
|
+
for (const item of completedRewinds) {
|
|
1103
|
+
this.upsertBlockResetNotice(item.blockId, {
|
|
1104
|
+
id: item.execId || `${item.blockId}-${item.isThinking ? 'thinking' : 'text'}`,
|
|
1105
|
+
message: item.message,
|
|
1106
|
+
isWaiting: true,
|
|
1107
|
+
streamType: item.isThinking ? 'thinking' : 'text'
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
this.activeResetNotices.update((notices) => {
|
|
1111
|
+
const next = new Map(notices);
|
|
1112
|
+
for (const item of completedRewinds) {
|
|
1113
|
+
next.set(item.blockId, item.message);
|
|
1114
|
+
}
|
|
1115
|
+
return next;
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
if (hasUpdates) {
|
|
1120
|
+
this.typedTextsSignal.update((r) => ({ ...r, ...updates }));
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Triggers fast typewriter rewind on a stream execution when a mid-stream reset occurs.
|
|
1126
|
+
* Once characters rewind to zero, the block displays the graceful retry notification.
|
|
1127
|
+
*/
|
|
1128
|
+
public triggerStreamRewind(execId: string | undefined, message: string): void {
|
|
1129
|
+
const blocks = untracked(() => this.consolidatedBlocks());
|
|
1130
|
+
const currentTexts = untracked(() => this.typedTextsSignal());
|
|
1131
|
+
|
|
1132
|
+
const targetBlock = blocks.find(b =>
|
|
1133
|
+
b.data?.execution_id === execId
|
|
1134
|
+
|| b.id === `stream-${execId}`
|
|
1135
|
+
|| b.data?.step_id === execId
|
|
1136
|
+
|| (Array.isArray(b.data?.stream_segments) && b.data.stream_segments.some((s: any) => s.execution_id === execId))
|
|
1137
|
+
) || blocks.find(b => b.data?.isReset);
|
|
1138
|
+
|
|
1139
|
+
if (!targetBlock) return;
|
|
1140
|
+
const blockId = targetBlock.id;
|
|
1141
|
+
|
|
1142
|
+
const keysToCheck: Array<{ key: string; isThinking: boolean }> = [
|
|
1143
|
+
{ key: blockId, isThinking: false },
|
|
1144
|
+
{ key: blockId + '-native', isThinking: true }
|
|
1145
|
+
];
|
|
1146
|
+
|
|
1147
|
+
let startedRewind = false;
|
|
1148
|
+
for (const { key, isThinking } of keysToCheck) {
|
|
1149
|
+
this.typingTargets.delete(key);
|
|
1150
|
+
|
|
1151
|
+
const currentText = currentTexts[key] || '';
|
|
1152
|
+
if (currentText.length > 0) {
|
|
1153
|
+
this.rewindTargets.set(key, {
|
|
1154
|
+
blockId,
|
|
1155
|
+
isThinking,
|
|
1156
|
+
execId,
|
|
1157
|
+
message
|
|
1158
|
+
});
|
|
1159
|
+
startedRewind = true;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (startedRewind) {
|
|
1164
|
+
this.ensureTypingLoop();
|
|
1165
|
+
} else {
|
|
1166
|
+
this.upsertBlockResetNotice(blockId, {
|
|
1167
|
+
id: execId || `${blockId}-text`,
|
|
1168
|
+
message,
|
|
1169
|
+
isWaiting: true,
|
|
1170
|
+
streamType: 'text'
|
|
1171
|
+
});
|
|
1172
|
+
this.activeResetNotices.update(notices => {
|
|
1173
|
+
const next = new Map(notices);
|
|
1174
|
+
next.set(blockId, message);
|
|
1175
|
+
return next;
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
public getBlockResets(block: any, isThinking: boolean = false): StreamResetNotice[] {
|
|
1181
|
+
if (!block) return [];
|
|
1182
|
+
const blockId = block.id;
|
|
1183
|
+
const targetStreamType = isThinking ? 'thinking' : 'text';
|
|
1184
|
+
|
|
1185
|
+
// 1. From activeBlockResets signal
|
|
1186
|
+
const activeList = (this.activeBlockResets().get(blockId) || [])
|
|
1187
|
+
.filter(r => r.streamType === targetStreamType);
|
|
1188
|
+
|
|
1189
|
+
// 2. From block.data.stream_resets (aggregated from logs)
|
|
1190
|
+
const blockDataResets = (Array.isArray(block.data?.stream_resets) ? block.data.stream_resets : [])
|
|
1191
|
+
.filter((r: any) => (r.streamType || 'text') === targetStreamType);
|
|
1192
|
+
|
|
1193
|
+
// Fast path: if both lists are empty, check fallback or return empty array immediately
|
|
1194
|
+
if (activeList.length === 0 && blockDataResets.length === 0) {
|
|
1195
|
+
if (!isThinking && block.data?.isReset && block.data?.resetMessage) {
|
|
1196
|
+
return [{
|
|
1197
|
+
id: block.data.execution_id || blockId,
|
|
1198
|
+
message: block.data.resetMessage,
|
|
1199
|
+
isWaiting: !block.data?.isCompleted,
|
|
1200
|
+
streamType: 'text'
|
|
1201
|
+
}];
|
|
1202
|
+
}
|
|
1203
|
+
return [];
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// Merge by id, giving precedence to activeList
|
|
1207
|
+
const mergedMap = new Map<string, StreamResetNotice>();
|
|
1208
|
+
for (const r of blockDataResets) {
|
|
1209
|
+
mergedMap.set(r.id, {
|
|
1210
|
+
id: r.id,
|
|
1211
|
+
message: r.message,
|
|
1212
|
+
isWaiting: block.data?.isCompleted ? false : (r.isWaiting ?? false),
|
|
1213
|
+
streamType: r.streamType || targetStreamType
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
for (const r of activeList) {
|
|
1217
|
+
mergedMap.set(r.id, {
|
|
1218
|
+
...r,
|
|
1219
|
+
isWaiting: block.data?.isCompleted ? false : r.isWaiting
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
return Array.from(mergedMap.values());
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
public hasWaitingReset(block: any, isThinking: boolean = false): boolean {
|
|
1227
|
+
const resets = this.getBlockResets(block, isThinking);
|
|
1228
|
+
return resets.some(r => r.isWaiting);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
public hasBlockResets(block: any, isThinking: boolean = false): boolean {
|
|
1232
|
+
return this.getBlockResets(block, isThinking).length > 0;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
public formatResetMessage(message: string, isWaiting: boolean): string {
|
|
1236
|
+
if (!message) return '';
|
|
1237
|
+
if (isWaiting) return message;
|
|
1238
|
+
return message
|
|
1239
|
+
.replace(/[,.\s]*(?:Retrying automatically(?:\.{3}|\.\.\.)?)\s*$/i, '')
|
|
1240
|
+
.trim();
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
public getCheckerSegmentResetMessage(item: any): string {
|
|
1244
|
+
return item?.data?.resetMessage || DEFAULT_STREAM_RESET_MESSAGE;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
public getResetNotice(block: any): string | null {
|
|
1248
|
+
if (!block) return null;
|
|
1249
|
+
const resets = this.getBlockResets(block, false);
|
|
1250
|
+
if (resets.length > 0) {
|
|
1251
|
+
const last = resets[resets.length - 1];
|
|
1252
|
+
return this.formatResetMessage(last.message, last.isWaiting);
|
|
1253
|
+
}
|
|
1254
|
+
const blockId = block.id;
|
|
1255
|
+
const active = this.activeResetNotices().get(blockId);
|
|
1256
|
+
if (active) return this.formatResetMessage(active, !block.data?.isCompleted);
|
|
1257
|
+
if (block.data?.isReset && block.data?.resetMessage) {
|
|
1258
|
+
return this.formatResetMessage(block.data.resetMessage, !block.data?.isCompleted);
|
|
1259
|
+
}
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
public isStreamRewinding(key: string): boolean {
|
|
1264
|
+
return this.rewindTargets.has(key);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
public isCheckerSegmentResetNotice(item: any): boolean {
|
|
1268
|
+
if (!item?.data?.isReset) return false;
|
|
1269
|
+
const text = item.data.text;
|
|
1270
|
+
return !text || text.trim().length === 0;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
public getStreamDisplayText(key: string, fallback: string = ''): string {
|
|
1274
|
+
const typed = this.typedTextsSignal()[key];
|
|
1275
|
+
return typed !== undefined ? typed : fallback;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
private triggerDelayedCollapse(execId: string) {
|
|
1279
|
+
if (!this.scheduledCollapses.has(execId)) {
|
|
1280
|
+
this.scheduledCollapses.add(execId);
|
|
1281
|
+
setTimeout(() => {
|
|
1282
|
+
this.collapsedStreams.update((set: Set<string>) => {
|
|
1283
|
+
const newSet = new Set(set);
|
|
1284
|
+
newSet.add(execId);
|
|
1285
|
+
return newSet;
|
|
1286
|
+
});
|
|
1287
|
+
}, 1000);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
public onDetailsToggle(event: Event, execId: string): void {
|
|
1292
|
+
const details = event.target as HTMLDetailsElement;
|
|
1293
|
+
const isOpen = details.open;
|
|
1294
|
+
this.collapsedStreams.update((set: Set<string>) => {
|
|
1295
|
+
const newSet = new Set(set);
|
|
1296
|
+
if (isOpen) {
|
|
1297
|
+
newSet.delete(execId);
|
|
1298
|
+
} else {
|
|
1299
|
+
newSet.add(execId);
|
|
1300
|
+
}
|
|
1301
|
+
return newSet;
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
public toggleCollapse(execId: string): void {
|
|
1306
|
+
this.collapsedStreams.update((set: Set<string>) => {
|
|
1307
|
+
const newSet = new Set(set);
|
|
1308
|
+
if (newSet.has(execId)) {
|
|
1309
|
+
newSet.delete(execId);
|
|
1310
|
+
} else {
|
|
1311
|
+
newSet.add(execId);
|
|
1312
|
+
}
|
|
1313
|
+
return newSet;
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Android Action Delegation Methods
|
|
1318
|
+
public getActionObject(action: any): any {
|
|
1319
|
+
return getActionObject(action);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
public isAndroidAction(action: any): boolean {
|
|
1323
|
+
return isAndroidAction(action);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
public getActionIcon(action: any): string {
|
|
1327
|
+
return getActionIcon(action);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
public getActionTitle(action: any): string {
|
|
1331
|
+
return getActionTitle(action);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
public getActionTargetText(action: any): string {
|
|
1335
|
+
return getActionTargetText(action);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
public getActionInputLabel(action: any): string {
|
|
1339
|
+
return getActionInputLabel(action);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
public getActionInputText(action: any): string {
|
|
1343
|
+
return getActionInputText(action);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
public getActionCoords(action: any): string {
|
|
1347
|
+
return getActionCoords(action);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
public isActionFailed(action: any, stepData?: any): boolean {
|
|
1351
|
+
return isActionFailed(action, stepData);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
public getActionErrorMessage(action: any, stepData?: any): string {
|
|
1355
|
+
return getActionErrorMessage(action, stepData);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
public isActionExpanded(cardId: string, defaultExpanded: boolean = false): boolean {
|
|
1359
|
+
if (this.collapsedActionCards().has(cardId)) return false;
|
|
1360
|
+
if (this.expandedActionCards().has(cardId)) return true;
|
|
1361
|
+
return defaultExpanded;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
public toggleActionExpanded(cardId: string, event?: Event, defaultExpanded: boolean = false): void {
|
|
1365
|
+
if (event) {
|
|
1366
|
+
event.stopPropagation();
|
|
1367
|
+
}
|
|
1368
|
+
const isCurrentlyExpanded = this.isActionExpanded(cardId, defaultExpanded);
|
|
1369
|
+
if (isCurrentlyExpanded) {
|
|
1370
|
+
this.expandedActionCards.update(set => {
|
|
1371
|
+
const newSet = new Set(set);
|
|
1372
|
+
newSet.delete(cardId);
|
|
1373
|
+
return newSet;
|
|
1374
|
+
});
|
|
1375
|
+
this.collapsedActionCards.update(set => {
|
|
1376
|
+
const newSet = new Set(set);
|
|
1377
|
+
newSet.add(cardId);
|
|
1378
|
+
return newSet;
|
|
1379
|
+
});
|
|
1380
|
+
} else {
|
|
1381
|
+
this.collapsedActionCards.update(set => {
|
|
1382
|
+
const newSet = new Set(set);
|
|
1383
|
+
newSet.delete(cardId);
|
|
1384
|
+
return newSet;
|
|
1385
|
+
});
|
|
1386
|
+
this.expandedActionCards.update(set => {
|
|
1387
|
+
const newSet = new Set(set);
|
|
1388
|
+
newSet.add(cardId);
|
|
1389
|
+
return newSet;
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
public getActionBounds(action: any): string {
|
|
1395
|
+
return getActionBounds(action);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
public getActionResourceId(action: any): string {
|
|
1399
|
+
return getActionResourceId(action);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
public getActionClass(action: any): string {
|
|
1403
|
+
return getActionClass(action);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
public getActionExtraParams(action: any): ActionParam[] {
|
|
1407
|
+
return extractActionExtraParams(action, this.actionParamsCache);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
public getToolExtraParams(toolData: any): ActionParam[] {
|
|
1411
|
+
return extractToolExtraParams(toolData, this.toolParamsCache);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
public getStepPreImageUrl(stepData: any, actionData?: any): string | null {
|
|
1415
|
+
return getStepPreImageUrl(stepData, actionData);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
public getStepPostImageUrl(stepData: any, actionData?: any): string | null {
|
|
1419
|
+
return getStepPostImageUrl(stepData, actionData);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Zoom Modal & Overlay Methods
|
|
1423
|
+
public openImageModal(url: string, actionObj?: any, event?: Event): void {
|
|
1424
|
+
if (event) {
|
|
1425
|
+
event.stopPropagation();
|
|
1426
|
+
}
|
|
1427
|
+
this.selectedZoomImage.set(url);
|
|
1428
|
+
this.selectedZoomAction.set(actionObj || null);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
public closeImageModal(event?: Event): void {
|
|
1432
|
+
if (event) {
|
|
1433
|
+
event.stopPropagation();
|
|
1434
|
+
}
|
|
1435
|
+
this.selectedZoomImage.set(null);
|
|
1436
|
+
this.selectedZoomAction.set(null);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
public onImageLoad(img: HTMLImageElement, overlay: HTMLElement, actionObj: any): void {
|
|
1440
|
+
if (!img || !overlay) return;
|
|
1441
|
+
try {
|
|
1442
|
+
this.drawActionCoordinatesOnOverlay(img, overlay, actionObj);
|
|
1443
|
+
} catch (e) {
|
|
1444
|
+
console.warn('Failed to draw action overlay:', e);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
public drawActionCoordinatesOnOverlay(img: HTMLImageElement, overlay: HTMLElement, actionObj: any): void {
|
|
1449
|
+
drawActionCoordinatesOnOverlay(img, overlay, actionObj);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Generic Tool Delegation Methods
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* First save_note call per note key, derived once per blocks change instead
|
|
1456
|
+
* of re-scanning every block for every rendered tool row.
|
|
1457
|
+
*/
|
|
1458
|
+
private firstSaveNoteByKey = computed<Map<string, any>>(() => {
|
|
1459
|
+
const firstByKey = new Map<string, any>();
|
|
1460
|
+
for (const block of this.consolidatedBlocks()) {
|
|
1461
|
+
if (block.type === 'step' && block.data.generic_tools) {
|
|
1462
|
+
for (const t of this.getUniqueGenericTools(block.data.generic_tools)) {
|
|
1463
|
+
if (t.name && t.name.toLowerCase() === 'save_note') {
|
|
1464
|
+
const key = this.getToolKey(t);
|
|
1465
|
+
if (key && !firstByKey.has(key)) {
|
|
1466
|
+
firstByKey.set(key, t);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return firstByKey;
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
public isFirstSaveNoteForKey(tool: any): boolean {
|
|
1476
|
+
if (!tool || !tool.name || tool.name.toLowerCase() !== 'save_note') {
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
const key = this.getToolKey(tool);
|
|
1480
|
+
if (!key) return true;
|
|
1481
|
+
|
|
1482
|
+
const firstCall = this.firstSaveNoteByKey().get(key);
|
|
1483
|
+
if (!firstCall) return true;
|
|
1484
|
+
return (tool.trace_id && firstCall.trace_id)
|
|
1485
|
+
? tool.trace_id === firstCall.trace_id
|
|
1486
|
+
: tool === firstCall;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
public getToolDisplayLabel(tool: any): string {
|
|
1490
|
+
return getToolDisplayLabel(tool, this.isFirstSaveNoteForKey(tool));
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/** Short plain-language phase for a compress_history line (hover title). */
|
|
1494
|
+
public getCompressionPhaseTitle(tool: any): string | null {
|
|
1495
|
+
return isCompressionTool(tool) ? getCompressionPhaseLabel(tool) : null;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* A generic tool line pulses only while work is in flight. A compression
|
|
1500
|
+
* whose summary is ready but held back by the context gate is waiting, not
|
|
1501
|
+
* running, even though its trace status is still `running`.
|
|
1502
|
+
*/
|
|
1503
|
+
public isToolLineRunning(item: any, block: any): boolean {
|
|
1504
|
+
return this.isItemRunning(item, block) && !isCompressionWaiting(item?.data);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
public isNoteTool(tool: any): boolean {
|
|
1508
|
+
return isNoteTool(tool);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
public isVideoTool(tool: any): boolean {
|
|
1512
|
+
return isVideoTool(tool);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
public getVideoToolTarget(tool: any): string | null {
|
|
1516
|
+
return getVideoToolTarget(tool);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
public getVideoAnalysisLabel(tool: any): string {
|
|
1520
|
+
return getVideoAnalysisView(tool)?.title || 'Analyzing screen recording';
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
public getVideoAnalysisRangeLabel(tool: any): string {
|
|
1524
|
+
const range = getVideoAnalysisView(tool)?.requestedRange;
|
|
1525
|
+
if (!range) return 'Screen recording';
|
|
1526
|
+
return `${formatVideoTime(range.start)}–${formatVideoTime(range.end)}`;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
public getVideoAnalysisDetail(tool: any): string {
|
|
1530
|
+
const view = getVideoAnalysisView(tool);
|
|
1531
|
+
if (!view || view.totalCount <= 1) return '';
|
|
1532
|
+
if (view.outcome === 'running' || view.outcome === 'recovering') {
|
|
1533
|
+
return view.completedCount > 0
|
|
1534
|
+
? `${view.completedCount}/${view.totalCount} segments saved`
|
|
1535
|
+
: '';
|
|
1536
|
+
}
|
|
1537
|
+
if (view.outcome === 'partial') {
|
|
1538
|
+
return `${view.completedCount}/${view.totalCount} segments saved`;
|
|
1539
|
+
}
|
|
1540
|
+
return '';
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
public isVideoAnalysisAttention(tool: any): boolean {
|
|
1544
|
+
const outcome = getVideoAnalysisView(tool)?.outcome;
|
|
1545
|
+
return outcome === 'partial' || outcome === 'failed';
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
public onVideoToolClick(toolData: any): void {
|
|
1549
|
+
const curSessionId = this.agentService.currentSessionId();
|
|
1550
|
+
if (curSessionId) {
|
|
1551
|
+
const start = getVideoAnalysisView(toolData)?.requestedRange?.start;
|
|
1552
|
+
this.agentService.openVideoPlayer(curSessionId, undefined, undefined, start);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
public getScreenRecordingButtonTitle(): string {
|
|
1557
|
+
if (this.agentService.isCurrentSessionRunning()) {
|
|
1558
|
+
return 'Task is currently running (Recording screen)';
|
|
1559
|
+
}
|
|
1560
|
+
if (this.agentService.currentSessionRecordingStatus() === 'processing') {
|
|
1561
|
+
return 'Preparing screen recording...';
|
|
1562
|
+
}
|
|
1563
|
+
if (this.agentService.currentSessionVideoUrl()) {
|
|
1564
|
+
return 'Play Screen Recording Video';
|
|
1565
|
+
}
|
|
1566
|
+
if (this.agentService.hasCurrentSessionStepFrames()) {
|
|
1567
|
+
return this.agentService.currentSessionRecordingStatus() === 'failed'
|
|
1568
|
+
? 'Video recording failed — Click to replay step-by-step screenshots'
|
|
1569
|
+
: 'Play step-by-step screenshots replay';
|
|
1570
|
+
}
|
|
1571
|
+
if (this.agentService.currentSessionRecordingStatus() === 'failed') {
|
|
1572
|
+
return 'Screen recording generation failed';
|
|
1573
|
+
}
|
|
1574
|
+
return 'Screen Recording';
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
public isDeviceActionTool(tool: any): boolean {
|
|
1578
|
+
return isDeviceActionTool(tool);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
public isFailureAnalyzerActionTool(tool: any): boolean {
|
|
1582
|
+
return isDeviceActionTool(tool);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
public getToolAgentName(tool: any): string | null {
|
|
1586
|
+
return getToolAgentName(tool);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
public shouldShowTool(tool: any, stepData?: any): boolean {
|
|
1590
|
+
return shouldShowTool(tool, stepData);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
public getUniqueGenericTools(tools: any[] | undefined): any[] {
|
|
1594
|
+
return getUniqueGenericTools(tools);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
public getSortedStepEvents(stepData: any): StepEvent[] {
|
|
1598
|
+
return getSortedStepEvents(stepData, this.sortedEventsCache);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/**
|
|
1602
|
+
* Memoized per block-data object: templates call these many times per
|
|
1603
|
+
* change-detection pass, and the raw-thinking path runs JSON-detection
|
|
1604
|
+
* heuristics over the full text.
|
|
1605
|
+
*/
|
|
1606
|
+
private getThinkingTexts(data: any): { native: string | null; raw: string | null } {
|
|
1607
|
+
let entry = this.thinkingTextCache.get(data);
|
|
1608
|
+
if (!entry) {
|
|
1609
|
+
const nativeText = data.operator_native_thinking || (data.stream_type === 'thinking' ? data.text : null);
|
|
1610
|
+
const rawText = data.operator_raw_thinking || (data.stream_type === 'text' ? data.text : null);
|
|
1611
|
+
entry = {
|
|
1612
|
+
native: nativeText && nativeText.trim() ? nativeText : null,
|
|
1613
|
+
raw: rawText && rawText.trim() && this.isHumanThinking(rawText) ? rawText : null
|
|
1614
|
+
};
|
|
1615
|
+
this.thinkingTextCache.set(data, entry);
|
|
1616
|
+
}
|
|
1617
|
+
return entry;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
public getNativeThinking(block: any): string | null {
|
|
1621
|
+
if (!block || !block.data) return null;
|
|
1622
|
+
return this.getThinkingTexts(block.data).native;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
public getRawThinking(block: any): string | null {
|
|
1626
|
+
if (!block || !block.data) return null;
|
|
1627
|
+
return this.getThinkingTexts(block.data).raw;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// --- Check blocks (verifier lane) ----------------------------------------------
|
|
1631
|
+
public isCheckerBlock(block: any): boolean {
|
|
1632
|
+
return block?.type === 'checker';
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
public isCheckerPhase(phase: any): boolean {
|
|
1636
|
+
const blocks: any[] = Array.isArray(phase?.blocks) ? phase.blocks : [];
|
|
1637
|
+
return blocks.length > 0 && blocks.every((b) => this.isCheckerBlock(b));
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
public getCheckerPhaseLabel(block: any): string {
|
|
1641
|
+
switch (block?.data?.phase) {
|
|
1642
|
+
case 'final':
|
|
1643
|
+
return 'Final check';
|
|
1644
|
+
case 'outcome':
|
|
1645
|
+
return 'Result';
|
|
1646
|
+
default:
|
|
1647
|
+
return 'Check';
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
public getCheckerVerdicts(block: any): any[] {
|
|
1652
|
+
return Array.isArray(block?.data?.verdicts) ? block.data.verdicts : [];
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/** Declared check items; persisted attempts only carry verdicts, so derive them. */
|
|
1656
|
+
public getCheckerItems(block: any): any[] {
|
|
1657
|
+
const d = block?.data || {};
|
|
1658
|
+
if (Array.isArray(d.items) && d.items.length > 0) return d.items;
|
|
1659
|
+
return this.getCheckerVerdicts(block).map((v) => ({ kind: v.kind, text: v.item_text, when: v.when }));
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/** How the item is judged, in plain words (verify = must pass, assert = recorded test result). */
|
|
1663
|
+
public getCheckMethodLabel(item: any, block: any): string {
|
|
1664
|
+
const parts: string[] = [item?.kind === 'assert' ? 'Test assertion' : 'Must pass'];
|
|
1665
|
+
if (item?.when === 'at_end' && block?.data?.phase !== 'final') parts.push('at the end');
|
|
1666
|
+
return parts.join(' · ');
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/** Superseded / unchecked attempts collapse to their header line. */
|
|
1670
|
+
public isCheckerMuted(block: any): boolean {
|
|
1671
|
+
const status = block?.data?.status;
|
|
1672
|
+
return status === 'superseded' || status === 'unchecked';
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
/** Number of the check item a verdict belongs to (1-based, matches the list above the stream). */
|
|
1676
|
+
public getVerdictItemNumber(block: any, verdict: any, fallbackIndex: number): number {
|
|
1677
|
+
const idx = this.getCheckerItems(block).findIndex((i) => i.text === verdict?.item_text);
|
|
1678
|
+
return (idx > -1 ? idx : fallbackIndex) + 1;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
public getVerdictStatusText(status: string): string {
|
|
1682
|
+
switch (status) {
|
|
1683
|
+
case 'passed':
|
|
1684
|
+
return 'Passed';
|
|
1685
|
+
case 'failed':
|
|
1686
|
+
return 'Failed';
|
|
1687
|
+
case 'inconclusive':
|
|
1688
|
+
return 'Could not be confirmed';
|
|
1689
|
+
case 'superseded':
|
|
1690
|
+
return 'Superseded by a newer check';
|
|
1691
|
+
case 'unchecked':
|
|
1692
|
+
return 'Not checked';
|
|
1693
|
+
default:
|
|
1694
|
+
return status || '';
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
public hasCheckerResult(block: any): boolean {
|
|
1699
|
+
if (!this.isCheckerBlock(block) || this.isBlockRunning(block) || this.isCheckerMuted(block)) return false;
|
|
1700
|
+
if (block.data?.phase === 'outcome') return this.getCheckerNotes(block).length > 0;
|
|
1701
|
+
return this.getCheckerVerdicts(block).length > 0 || this.getCheckerNotes(block).length > 0;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/** Follow-ups not already visible in the verdict rows (the findings text repeats them). */
|
|
1705
|
+
public getCheckerNotes(block: any): string[] {
|
|
1706
|
+
const d = block?.data || {};
|
|
1707
|
+
if (d.phase === 'outcome') return Array.isArray(d.last_findings) ? d.last_findings : [];
|
|
1708
|
+
const notes: string[] = [];
|
|
1709
|
+
if (Array.isArray(d.unmet_subgoals)) {
|
|
1710
|
+
for (const text of d.unmet_subgoals) notes.push(`Not finished yet: ${text}`);
|
|
1711
|
+
}
|
|
1712
|
+
if (d.reverted) notes.push('The step was sent back to be redone.');
|
|
1713
|
+
if (d.error) notes.push(String(d.error));
|
|
1714
|
+
return notes;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Streamed turns of a check: the last one is live while the block runs;
|
|
1718
|
+
// finished Thought turns fold unless the reader opens them.
|
|
1719
|
+
public openCheckSegments = signal<Set<string>>(new Set<string>());
|
|
1720
|
+
|
|
1721
|
+
public isCheckerSegmentRunning(block: any, itemIdx: number): boolean {
|
|
1722
|
+
if (!this.isBlockRunning(block)) return false;
|
|
1723
|
+
return itemIdx === this.getSortedStepEvents(block.data).length - 1;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
public isCheckerSegmentCollapsed(block: any, itemIdx: number): boolean {
|
|
1727
|
+
if (this.isCheckerSegmentRunning(block, itemIdx)) return false;
|
|
1728
|
+
return !this.openCheckSegments().has(`${block.id}-${itemIdx}`);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
public toggleCheckerSegment(block: any, itemIdx: number): void {
|
|
1732
|
+
const key = `${block.id}-${itemIdx}`;
|
|
1733
|
+
this.openCheckSegments.update((set) => {
|
|
1734
|
+
const next = new Set(set);
|
|
1735
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
1736
|
+
return next;
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
public isCheckerTextVisible(item: any): boolean {
|
|
1741
|
+
if (item?.data?.isReset) return true;
|
|
1742
|
+
const text = item?.data?.text;
|
|
1743
|
+
return typeof text === 'string' && text.trim().length > 0 && this.isHumanThinking(text);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
public getCheckerTone(block: any): string {
|
|
1747
|
+
const d = block?.data || {};
|
|
1748
|
+
if (d.phase === 'outcome') {
|
|
1749
|
+
return d.task_status === 'completed' ? 'passed' : (d.task_status === 'blocked' ? 'failed' : 'inconclusive');
|
|
1750
|
+
}
|
|
1751
|
+
if (d.isCompleted === false) return 'running';
|
|
1752
|
+
if (d.status === 'superseded' || d.status === 'unchecked') return 'muted';
|
|
1753
|
+
if (d.status === 'error') return 'inconclusive';
|
|
1754
|
+
const verdicts = this.getCheckerVerdicts(block);
|
|
1755
|
+
if (verdicts.some((v) => v.status === 'failed')) return 'failed';
|
|
1756
|
+
if (verdicts.some((v) => v.status === 'inconclusive')) return 'inconclusive';
|
|
1757
|
+
return 'passed';
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
public getCheckerStatusLabel(block: any): string {
|
|
1761
|
+
const d = block?.data || {};
|
|
1762
|
+
if (d.phase === 'outcome') {
|
|
1763
|
+
const t = d.tests || {};
|
|
1764
|
+
const label = d.task_status === 'completed' ? 'Goal completed'
|
|
1765
|
+
: (d.task_status === 'blocked' ? 'Blocked' : 'Partially completed');
|
|
1766
|
+
const counts = [
|
|
1767
|
+
[t.passed, 'passed'],
|
|
1768
|
+
[t.failed, 'failed'],
|
|
1769
|
+
[t.inconclusive, 'inconclusive'],
|
|
1770
|
+
[t.unchecked, 'not checked']
|
|
1771
|
+
]
|
|
1772
|
+
.filter(([n]) => Number(n) > 0)
|
|
1773
|
+
.map(([n, word]) => `${n} ${word}`);
|
|
1774
|
+
return counts.length > 0 ? `${label} · ${counts.join(' · ')}` : label;
|
|
1775
|
+
}
|
|
1776
|
+
if (d.isCompleted === false) return 'Checking…';
|
|
1777
|
+
switch (d.status) {
|
|
1778
|
+
case 'superseded':
|
|
1779
|
+
return 'Superseded';
|
|
1780
|
+
case 'unchecked':
|
|
1781
|
+
return 'Not checked';
|
|
1782
|
+
case 'error':
|
|
1783
|
+
return 'No verdict';
|
|
1784
|
+
}
|
|
1785
|
+
const verdicts = this.getCheckerVerdicts(block);
|
|
1786
|
+
const failed = verdicts.filter((v) => v.status === 'failed').length;
|
|
1787
|
+
if (failed > 0) return `${failed} failed`;
|
|
1788
|
+
if (verdicts.some((v) => v.status === 'inconclusive')) return 'Inconclusive';
|
|
1789
|
+
return verdicts.length > 0 ? 'Passed' : 'Done';
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
public getVerdictIcon(status: string): string {
|
|
1793
|
+
switch (status) {
|
|
1794
|
+
case 'passed':
|
|
1795
|
+
return 'check_circle';
|
|
1796
|
+
case 'failed':
|
|
1797
|
+
return 'cancel';
|
|
1798
|
+
case 'inconclusive':
|
|
1799
|
+
return 'help';
|
|
1800
|
+
case 'superseded':
|
|
1801
|
+
return 'history';
|
|
1802
|
+
case 'unchecked':
|
|
1803
|
+
return 'radio_button_unchecked';
|
|
1804
|
+
default:
|
|
1805
|
+
return 'pending';
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
public hasVisibleContent(block: any): boolean {
|
|
1810
|
+
if (!block) return false;
|
|
1811
|
+
if (block.type === 'checker') return true;
|
|
1812
|
+
const hasNative = Boolean(this.getNativeThinking(block));
|
|
1813
|
+
const hasRaw = Boolean(this.getRawThinking(block));
|
|
1814
|
+
const hasReset = this.hasBlockResets(block, false) || this.hasBlockResets(block, true);
|
|
1815
|
+
const hasVisibleTools = Boolean(block.data?.generic_tools) && block.data.generic_tools.some((t: any) =>
|
|
1816
|
+
this.shouldShowTool(t, block.data)
|
|
1817
|
+
|| this.isDisplayableLLMFailure(t)
|
|
1818
|
+
|| this.isLLMRetry(t)
|
|
1819
|
+
|| this.isReportStatusAction(t)
|
|
1820
|
+
|| this.isBackendSwitchNote(t)
|
|
1821
|
+
);
|
|
1822
|
+
const hasAndroidActions = Boolean(block.data?.action_taken) && (this.isAndroidAction(block.data.action_taken) || this.isReportStatusAction(block.data.action_taken));
|
|
1823
|
+
return hasNative || hasRaw || hasReset || hasVisibleTools || hasAndroidActions;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
public isReportStatusAction(action: any): boolean {
|
|
1827
|
+
return isReportStatusAction(action);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
public getReportStatusValue(action: any): string {
|
|
1831
|
+
return getReportStatusValue(action);
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
public getReportStatusExplanation(action: any): string {
|
|
1835
|
+
return getReportStatusExplanation(action);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
public isHumanThinking(text: string | null): boolean {
|
|
1839
|
+
return isHumanThinking(text);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
public isTerminalLLMFailure(tool: any): boolean {
|
|
1843
|
+
return !!tool && this.retryablePauseTrace() === tool;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
public isDisplayableLLMFailure(tool: any): boolean {
|
|
1847
|
+
return tool?.type === 'llm_call'
|
|
1848
|
+
&& tool?.status === 'failed'
|
|
1849
|
+
&& (tool?.name === 'llm_pause' || tool?.payload?.pause === true);
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
/** Mid-run change of the UI-hierarchy source (helper <-> UIAutomator2). */
|
|
1853
|
+
public isBackendSwitchNote(tool: any): boolean {
|
|
1854
|
+
return isBackendSwitchNote(tool);
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
public getBackendSwitchMessage(tool: any): string {
|
|
1858
|
+
return String(tool?.payload?.message || 'UI hierarchy source changed');
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
public resumePausedTask(event: Event): void {
|
|
1862
|
+
event.stopPropagation();
|
|
1863
|
+
this.agentService.resumeTask();
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
public getLLMErrorText(tool: any): string {
|
|
1867
|
+
const noDetails = 'The AI provider did not return error details after the request failed.';
|
|
1868
|
+
if (!tool) return noDetails;
|
|
1869
|
+
const rawError = tool.payload?.error || tool.error;
|
|
1870
|
+
if (!rawError) return noDetails;
|
|
1871
|
+
const cleaned = this.cleanErrorMessage(rawError);
|
|
1872
|
+
return cleaned === 'Unknown error' ? noDetails : cleaned;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
public isLLMRetry(tool: any): boolean {
|
|
1876
|
+
return tool?.type === 'llm_call'
|
|
1877
|
+
&& tool?.status === 'retrying'
|
|
1878
|
+
&& tool?.payload?.source === 'provider_sdk'
|
|
1879
|
+
&& ['google', 'gemini'].includes(String(tool?.payload?.provider || '').toLowerCase());
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
public getLLMRetryEntries(tool: any): any[] {
|
|
1883
|
+
const retries = tool?.payload?.retries;
|
|
1884
|
+
return Array.isArray(retries) && retries.length > 0 ? retries : [tool?.payload || tool];
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
public getLLMFailureRetryEntries(tool: any): any[] {
|
|
1888
|
+
const retries = tool?.payload?.retries;
|
|
1889
|
+
if (!Array.isArray(retries)) return [];
|
|
1890
|
+
return retries.filter((retry: any) =>
|
|
1891
|
+
retry?.source === 'provider_sdk'
|
|
1892
|
+
&& ['google', 'gemini'].includes(String(retry?.provider || '').toLowerCase())
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
public hasLLMFailureRetryEntries(tool: any): boolean {
|
|
1897
|
+
return this.getLLMFailureRetryEntries(tool).length > 0;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
public getLLMRetryEntryDelay(entry: any): string | null {
|
|
1901
|
+
const delay = Number(entry?.delay);
|
|
1902
|
+
if (!Number.isFinite(delay) || delay <= 0) return null;
|
|
1903
|
+
return `${delay.toFixed(2).replace(/\.00$/, '')}s`;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
public getLLMRetryEntryWaited(entry: any): string {
|
|
1907
|
+
const delay = Number(entry?.delay);
|
|
1908
|
+
if (!Number.isFinite(delay) || delay <= 0) return '0s';
|
|
1909
|
+
|
|
1910
|
+
const rawStartedAt = Number(entry?.scheduled_at ?? entry?.timestamp);
|
|
1911
|
+
if (!Number.isFinite(rawStartedAt) || rawStartedAt <= 0) {
|
|
1912
|
+
return this.formatLLMDuration(delay);
|
|
1913
|
+
}
|
|
1914
|
+
const startedAtMs = rawStartedAt < 1e11 ? rawStartedAt * 1000 : rawStartedAt;
|
|
1915
|
+
const elapsed = Math.min(delay, Math.max(0, (this.retryClock() - startedAtMs) / 1000));
|
|
1916
|
+
return this.formatLLMDuration(elapsed);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
public isLLMRetryEntryWaiting(entry: any): boolean {
|
|
1920
|
+
const delay = Number(entry?.delay);
|
|
1921
|
+
const rawStartedAt = Number(entry?.scheduled_at ?? entry?.timestamp);
|
|
1922
|
+
if (!Number.isFinite(delay) || delay <= 0 || !Number.isFinite(rawStartedAt)) return false;
|
|
1923
|
+
const startedAtMs = rawStartedAt < 1e11 ? rawStartedAt * 1000 : rawStartedAt;
|
|
1924
|
+
return this.retryClock() < startedAtMs + delay * 1000;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
public getLLMFailureWaited(tool: any): string | null {
|
|
1928
|
+
const waited = Number(tool?.payload?.waited_seconds);
|
|
1929
|
+
if (!Number.isFinite(waited) || waited < 0) return null;
|
|
1930
|
+
return this.formatLLMDuration(waited);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
public formatLLMDuration(seconds: number): string {
|
|
1934
|
+
if (seconds < 0.05) return '0s';
|
|
1935
|
+
if (seconds < 10) return `${seconds.toFixed(1).replace(/\.0$/, '')}s`;
|
|
1936
|
+
return `${Math.round(seconds)}s`;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
public cleanErrorMessage(rawError: any): string {
|
|
1940
|
+
return cleanErrorMessage(rawError);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
public getToolKey(tool: any): string | null {
|
|
1944
|
+
return getToolKey(tool);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
public getToolCallName(action: any): string {
|
|
1948
|
+
const act = this.getActionObject(action);
|
|
1949
|
+
if (!act) return 'Tool';
|
|
1950
|
+
const label = this.getToolDisplayLabel(act);
|
|
1951
|
+
const key = this.getToolKey(act);
|
|
1952
|
+
return key ? `${label}: ${key}` : label;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
public getToolCallLabel(action: any): string {
|
|
1956
|
+
return this.getToolCallName(action);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
public isToolFailed(tool: any): boolean {
|
|
1960
|
+
return isToolFailed(tool);
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
public getToolErrorMessage(tool: any): string {
|
|
1964
|
+
return getToolErrorMessage(tool);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
public getToolArgs(tool: any): any {
|
|
1968
|
+
return getToolArgs(tool);
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
public getToolIcon(tool: any): string {
|
|
1972
|
+
return getToolIcon(tool);
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
public getToolTitle(tool: any): string {
|
|
1976
|
+
return getToolTitle(tool);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
public getToolTargetText(tool: any): string {
|
|
1980
|
+
return getToolTargetText(tool);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
public getToolInputLabel(tool: any): string {
|
|
1984
|
+
return getToolInputLabel(tool);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
public getToolInputText(tool: any): string {
|
|
1988
|
+
return getToolInputText(tool);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
public getToolCoords(tool: any): string {
|
|
1992
|
+
return getToolCoords(tool);
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
public getToolAnalysisText(tool: any): string {
|
|
1996
|
+
return getToolAnalysisText(tool);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
public getToolGenericDetails(tool: any): string {
|
|
2000
|
+
return getToolGenericDetails(tool);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
public isAdbCommandTool(tool: any): boolean {
|
|
2004
|
+
return isAdbCommandTool(tool);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
public getAdbCommandLine(tool: any): string {
|
|
2008
|
+
return getAdbCommandLine(tool);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
public getAdbCwd(tool: any): string {
|
|
2012
|
+
return getAdbCwd(tool);
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
public getAdbTerminalId(tool: any): string {
|
|
2016
|
+
return getAdbTerminalId(tool);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
public onSessionChange(event: Event): void {
|
|
2020
|
+
const selectElement = event.target as HTMLSelectElement;
|
|
2021
|
+
const sessionId = selectElement.value;
|
|
2022
|
+
if (sessionId) {
|
|
2023
|
+
this.agentService.selectSession(sessionId, true);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
public isStepEvent(eventType: string): boolean {
|
|
2028
|
+
return eventType === 'step_recorded' || eventType === 'step_updated';
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
public getCheckerResult(text: string): CheckerResult | null {
|
|
2032
|
+
return extractCheckerResult(text);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
public isShortContent(data: any): boolean {
|
|
2036
|
+
if (!data) return true;
|
|
2037
|
+
const str = typeof data === 'string' ? data : JSON.stringify(data);
|
|
2038
|
+
return str.length < 200;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
public formatJson(data: any): string {
|
|
2042
|
+
if (typeof data === 'string') {
|
|
2043
|
+
try {
|
|
2044
|
+
return JSON.stringify(JSON.parse(data), null, 2);
|
|
2045
|
+
} catch {
|
|
2046
|
+
return data;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
return JSON.stringify(data, null, 2);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
public getStatusLabel(status: string): string {
|
|
2053
|
+
switch (status) {
|
|
2054
|
+
case 'idle':
|
|
2055
|
+
return 'Idle';
|
|
2056
|
+
case 'running':
|
|
2057
|
+
return 'Running';
|
|
2058
|
+
case 'completed':
|
|
2059
|
+
return 'Completed';
|
|
2060
|
+
case 'offline':
|
|
2061
|
+
return 'Offline';
|
|
2062
|
+
default:
|
|
2063
|
+
return status.toUpperCase();
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* Render markdown with a per-slot memo. Returning the identical cached
|
|
2069
|
+
* string instance for unchanged text means the [innerHTML] binding sees the
|
|
2070
|
+
* same reference and skips DOM/sanitizer work entirely.
|
|
2071
|
+
*/
|
|
2072
|
+
public renderMarkdown(text: string, cacheKey?: string): string {
|
|
2073
|
+
if (!text) return '';
|
|
2074
|
+
if (!cacheKey) {
|
|
2075
|
+
return renderMarkdownToHtml(text);
|
|
2076
|
+
}
|
|
2077
|
+
const cached = this.markdownHtmlCache.get(cacheKey);
|
|
2078
|
+
if (cached && cached.source === text) {
|
|
2079
|
+
return cached.html;
|
|
2080
|
+
}
|
|
2081
|
+
const html = renderMarkdownToHtml(text);
|
|
2082
|
+
this.markdownHtmlCache.set(cacheKey, { source: text, html });
|
|
2083
|
+
return html;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
public onToolKeyClick(key: string): void {
|
|
2087
|
+
this.agentService.selectedNoteKey.set(key);
|
|
2088
|
+
this.agentService.activeTab.set('notes');
|
|
2089
|
+
|
|
2090
|
+
const sessionId = this.agentService.currentSessionId();
|
|
2091
|
+
if (sessionId) {
|
|
2092
|
+
this.agentService.fetchNotes(sessionId);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
public trackSession(index: number, session: Session): string {
|
|
2097
|
+
return session.session_id;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
public trackNoteKey(index: number, key: string): string {
|
|
2101
|
+
return key;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
public trackMilestone(index: number, milestone: NoteMilestone): number {
|
|
2105
|
+
return milestone.index;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
public trackMarkdownLine(index: number, line: MarkdownLine): number {
|
|
2109
|
+
return index;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
public trackPhase(index: number, phase: any): string {
|
|
2113
|
+
return phase.id;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
public trackBlock(index: number, block: any): string {
|
|
2117
|
+
return block.id;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
public trackTool(index: number, tool: any): string {
|
|
2121
|
+
return tool.trace_id || index.toString();
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
public trackStepEvent(index: number, item: { type: string; data: any; timestamp?: number }): string {
|
|
2125
|
+
if (!item || !item.data) return index.toString();
|
|
2126
|
+
// A step has at most one thinking and one text event; keying them by type
|
|
2127
|
+
// alone keeps their DOM stable while the streamed text grows (keying by
|
|
2128
|
+
// text length would tear down and rebuild the node on every chunk).
|
|
2129
|
+
if (item.type === 'thinking' || item.type === 'text') {
|
|
2130
|
+
return item.type;
|
|
2131
|
+
}
|
|
2132
|
+
return item.type + '-' + (item.data.trace_id
|
|
2133
|
+
|| item.data.timestamp || item.data.start_time || item.data.created_at || item.data.id
|
|
2134
|
+
|| ((item.data.action || item.data.name || '') + '-' + index));
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
public trackParam(index: number, param: { key: string; value: string }): string {
|
|
2138
|
+
return param.key;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
public getModelDisplayName(name: string | undefined | null): string {
|
|
2142
|
+
if (!name) return 'Flash';
|
|
2143
|
+
const lower = name.toLowerCase();
|
|
2144
|
+
if (lower.includes('pro')) return 'Pro';
|
|
2145
|
+
if (lower.includes('flash')) return 'Flash';
|
|
2146
|
+
return name.replace(/^artemis\s+/i, '');
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
public getModelIcon(name: string | undefined | null): string {
|
|
2150
|
+
if (!name) return 'bolt';
|
|
2151
|
+
const lower = name.toLowerCase();
|
|
2152
|
+
if (lower.includes('pro')) return 'diamond';
|
|
2153
|
+
if (lower.includes('flash')) return 'bolt';
|
|
2154
|
+
return 'smart_toy';
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
public getModelClass(name: string | undefined | null): string {
|
|
2158
|
+
if (!name) return 'is-flash';
|
|
2159
|
+
const lower = name.toLowerCase();
|
|
2160
|
+
if (lower.includes('pro')) return 'is-pro';
|
|
2161
|
+
if (lower.includes('flash')) return 'is-flash';
|
|
2162
|
+
return '';
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
public getArchitectureTooltip(model?: ModelInfo | null): string {
|
|
2166
|
+
if (!model) return 'Agent Architecture: ARTEMIS Flash (Reactive Fast Loop)';
|
|
2167
|
+
const name = this.getModelDisplayName(model.name);
|
|
2168
|
+
const isPro = name.toLowerCase().includes('pro');
|
|
2169
|
+
const archDesc = isPro
|
|
2170
|
+
? 'ARTEMIS Pro (Multi-Agent Cognitive State Graph)'
|
|
2171
|
+
: 'ARTEMIS Flash (Reactive Fast Loop)';
|
|
2172
|
+
if (model.id) {
|
|
2173
|
+
return `Agent Architecture: ${archDesc} · LLM: ${model.id} (${model.provider || 'google'})`;
|
|
2174
|
+
}
|
|
2175
|
+
return `Agent Architecture: ${archDesc}`;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
public formatTokenCount(tokens?: number): string {
|
|
2179
|
+
return formatTokenCount(tokens);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
public getTokenTooltip(phase: PhaseBlock): string {
|
|
2183
|
+
if (!phase.tokens) return '';
|
|
2184
|
+
if (phase.promptTokens && phase.completionTokens) {
|
|
2185
|
+
return `Consumed: ${phase.tokens.toLocaleString()} tokens (${phase.promptTokens.toLocaleString()} in / ${phase.completionTokens.toLocaleString()} out)`;
|
|
2186
|
+
}
|
|
2187
|
+
return `Consumed: ${phase.tokens.toLocaleString()} tokens`;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
public isCurrentSessionActive(): boolean {
|
|
2191
|
+
const status = this.agentService.agentStatus();
|
|
2192
|
+
if (status !== 'running') return false;
|
|
2193
|
+
const currentId = this.agentService.currentSessionId();
|
|
2194
|
+
const runningId = this.agentService.runningSessionId();
|
|
2195
|
+
if (currentId && runningId && currentId !== runningId) {
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
const currentSession = this.agentService.sessions().find(s => s.session_id === currentId);
|
|
2199
|
+
if (currentSession && (currentSession.status === 'completed' || currentSession.status === 'failed' || currentSession.status === 'cancelled')) {
|
|
2200
|
+
return false;
|
|
2201
|
+
}
|
|
2202
|
+
return true;
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
public isItemRunning(item: any, block: any): boolean {
|
|
2206
|
+
if (!item || !item.data) return false;
|
|
2207
|
+
if (!this.isCurrentSessionActive()) return false;
|
|
2208
|
+
if (item.data.status === 'success' || item.data.status === 'failed' || item.data.status === 'cancelled') return false;
|
|
2209
|
+
if (this.isReportStatusAction(item.data) || this.isReportStatusAction(item.data?.action_taken)) return false;
|
|
2210
|
+
if (item.data.status === 'running') return true;
|
|
2211
|
+
|
|
2212
|
+
// Check if session is currently running and this is the active trailing item
|
|
2213
|
+
const blocks = this.consolidatedBlocks();
|
|
2214
|
+
const lastBlock = blocks.length > 0 ? blocks[blocks.length - 1] : null;
|
|
2215
|
+
if (lastBlock && (lastBlock.id === block?.id || !block?.data?.isCompleted)) {
|
|
2216
|
+
const events = this.getSortedStepEvents(block.data);
|
|
2217
|
+
const lastEvent = events.length > 0 ? events[events.length - 1] : null;
|
|
2218
|
+
if (lastEvent && (lastEvent.data?.trace_id === item.data?.trace_id || lastEvent === item)) {
|
|
2219
|
+
return true;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
public isBlockRunning(block: any): boolean {
|
|
2226
|
+
if (!block || !block.data) return false;
|
|
2227
|
+
if (!this.isCurrentSessionActive()) return false;
|
|
2228
|
+
if (block.type === 'checker') return block.data.isCompleted === false;
|
|
2229
|
+
if (this.isReportStatusAction(block.data.action_taken) || block.data?.status === 'completed' || block.data?.status === 'failed' || block.data?.status === 'cancelled') return false;
|
|
2230
|
+
if (block.data.isCompleted === false) return true;
|
|
2231
|
+
const blocks = this.consolidatedBlocks();
|
|
2232
|
+
const lastBlock = blocks.length > 0 ? blocks[blocks.length - 1] : null;
|
|
2233
|
+
return lastBlock?.id === block.id;
|
|
2234
|
+
}
|
|
2235
|
+
}
|