polyrob 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- webview/webgate.py +196 -0
|
@@ -0,0 +1,3178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Manager for Task Agent WebView
|
|
3
|
+
*
|
|
4
|
+
* Manages chat-style display of agent progress and user interactions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { escapeHtml, formatCost, on, emit, off, sessionState, logger, RateLimiter, ExponentialBackoff } from '/static/js/ui-utils.js?v=5';
|
|
8
|
+
import { eventStore } from '/static/js/event-store.js?v=5';
|
|
9
|
+
import { chatFilter } from '/static/js/event-filter.js?v=5';
|
|
10
|
+
import { rafThrottle } from '/static/js/performance-utils.js?v=5';
|
|
11
|
+
import { FileAttachmentManager } from '/static/js/file-attachments.js?v=5';
|
|
12
|
+
|
|
13
|
+
export class ChatManager {
|
|
14
|
+
// Maximum messages to keep in memory to prevent unbounded growth
|
|
15
|
+
static MAX_MESSAGES = 1000;
|
|
16
|
+
|
|
17
|
+
constructor(sessionId) { // Remove socket param - will be set later
|
|
18
|
+
// Use centralized session state instead of manual tracking
|
|
19
|
+
this.sessionId = sessionState.sessionId || sessionId;
|
|
20
|
+
this.socket = null; // Will be set by connectToSocket()
|
|
21
|
+
this.isNewSession = sessionState.isNew;
|
|
22
|
+
|
|
23
|
+
// State
|
|
24
|
+
this.messages = [];
|
|
25
|
+
this.currentAgentMessage = null;
|
|
26
|
+
this.isStreaming = false;
|
|
27
|
+
this.scrollLocked = false;
|
|
28
|
+
|
|
29
|
+
// Track current agent message ID for proper event sequencing
|
|
30
|
+
this.currentAgentMessageId = null;
|
|
31
|
+
|
|
32
|
+
// Session state tracking (paused removed - use cancelled for interruption)
|
|
33
|
+
this.sessionStatus = null; // running, completed, cancelled, etc.
|
|
34
|
+
|
|
35
|
+
// Action registry (from available_actions event)
|
|
36
|
+
this.actionRegistry = {}; // { action_name: service_name }
|
|
37
|
+
|
|
38
|
+
// Pending messages queue (messages sent but not yet confirmed by feed)
|
|
39
|
+
this.pendingMessages = []; // [{text, timestamp, id}]
|
|
40
|
+
|
|
41
|
+
// Chunk-based message tracking (for discrete timestamped chunks)
|
|
42
|
+
this._currentIteration = 0; // Current iteration number for grouping
|
|
43
|
+
this._chunkGroups = new Map(); // groupId -> {chunks: [], collapsed: false}
|
|
44
|
+
|
|
45
|
+
// DOM elements
|
|
46
|
+
this.chatContainer = document.getElementById('chat-messages');
|
|
47
|
+
this.inputField = document.getElementById('chat-input');
|
|
48
|
+
this.sendButton = document.getElementById('chat-send-btn');
|
|
49
|
+
this.scrollButton = document.getElementById('scroll-to-bottom');
|
|
50
|
+
this.emptyState = this.chatContainer?.querySelector('.chat-empty-state');
|
|
51
|
+
|
|
52
|
+
// Track processed event IDs (EventStore handles dedup, this is for rendering)
|
|
53
|
+
this._processedEventIds = new Set();
|
|
54
|
+
|
|
55
|
+
// File attachment manager (will be initialized in init() after DOM is ready)
|
|
56
|
+
this.fileManager = null;
|
|
57
|
+
|
|
58
|
+
// Rate limiter for message sending (1 message per second)
|
|
59
|
+
this.messageRateLimiter = new RateLimiter(1000);
|
|
60
|
+
|
|
61
|
+
// Reconnection backoff
|
|
62
|
+
this.reconnectBackoff = new ExponentialBackoff({ maxRetries: 10 });
|
|
63
|
+
|
|
64
|
+
// Store bound event handlers for cleanup
|
|
65
|
+
this._boundHandlers = {
|
|
66
|
+
sendClick: null,
|
|
67
|
+
inputKeydown: null,
|
|
68
|
+
inputPaste: null,
|
|
69
|
+
inputChange: null,
|
|
70
|
+
scrollThrottled: null,
|
|
71
|
+
scrollButtonClick: null,
|
|
72
|
+
containerClick: null,
|
|
73
|
+
documentKeydown: null,
|
|
74
|
+
viewportResize: null
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Track if destroyed
|
|
78
|
+
this._destroyed = false;
|
|
79
|
+
|
|
80
|
+
// Initialize
|
|
81
|
+
if (this.chatContainer && this.inputField && this.sendButton) {
|
|
82
|
+
this.init();
|
|
83
|
+
} else {
|
|
84
|
+
logger.warn('[Chat] Required DOM elements not found, initialization skipped');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
init() {
|
|
89
|
+
logger.debug('[Chat] Initializing for session:', this.sessionId, 'isNew:', this.isNewSession);
|
|
90
|
+
|
|
91
|
+
// Initialize file attachment manager now that DOM is ready
|
|
92
|
+
try {
|
|
93
|
+
this.fileManager = new FileAttachmentManager();
|
|
94
|
+
logger.debug('[Chat] ā FileAttachmentManager initialized');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
logger.error('[Chat] ā FileAttachmentManager initialization FAILED:', error);
|
|
97
|
+
this.fileManager = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check session ownership and set up read-only mode if needed
|
|
101
|
+
this.checkOwnershipAndSetupReadOnly();
|
|
102
|
+
|
|
103
|
+
// Set up UI listeners (always needed)
|
|
104
|
+
this.setupUIListeners();
|
|
105
|
+
|
|
106
|
+
// Set up custom event listeners IMMEDIATELY (before socket)
|
|
107
|
+
// This ensures we receive events even if socket connects later
|
|
108
|
+
this.setupCustomEventListeners();
|
|
109
|
+
|
|
110
|
+
if (this.isNewSession) {
|
|
111
|
+
// New session mode - don't load history but DO setup socket listeners
|
|
112
|
+
// so we're ready when first message is sent
|
|
113
|
+
logger.debug('[Chat] New session mode - waiting for user input');
|
|
114
|
+
this.connectToSocket(); // Connect now so we're ready
|
|
115
|
+
} else {
|
|
116
|
+
// Active session mode - subscribe to EventStore and connect socket
|
|
117
|
+
logger.debug('[Chat] Active session - subscribing to EventStore');
|
|
118
|
+
this.subscribeToEventStore();
|
|
119
|
+
this.loadInitialTask();
|
|
120
|
+
this.connectToSocket();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Subscribe to EventStore for all event updates.
|
|
126
|
+
* This replaces loadHistory() - EventStore is populated by session.js
|
|
127
|
+
*/
|
|
128
|
+
subscribeToEventStore() {
|
|
129
|
+
logger.debug('[Chat] Subscribing to EventStore');
|
|
130
|
+
|
|
131
|
+
// Subscribe to all events
|
|
132
|
+
this._eventStoreUnsubscribe = eventStore.subscribe('all', (change) => {
|
|
133
|
+
if (change.action === 'insert') {
|
|
134
|
+
this.processEventFromStore(change.event);
|
|
135
|
+
} else if (change.action === 'batch') {
|
|
136
|
+
// Process batch in order
|
|
137
|
+
change.events.forEach(event => this.processEventFromStore(event));
|
|
138
|
+
} else if (change.action === 'clear') {
|
|
139
|
+
// Session reset - clear chat
|
|
140
|
+
this.messages = [];
|
|
141
|
+
this._processedEventIds.clear();
|
|
142
|
+
if (this.chatContainer) {
|
|
143
|
+
this.chatContainer.innerHTML = '';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Process any existing events in EventStore (from session.js initial load)
|
|
149
|
+
const existingEvents = eventStore.getAll();
|
|
150
|
+
if (existingEvents.length > 0) {
|
|
151
|
+
logger.debug(`[Chat] Processing ${existingEvents.length} existing events from EventStore`);
|
|
152
|
+
existingEvents.forEach(event => this.processEventFromStore(event));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Process a single event from EventStore
|
|
158
|
+
* NOTE: Duplicate check and marking is handled by onFeedUpdate()
|
|
159
|
+
*/
|
|
160
|
+
processEventFromStore(event) {
|
|
161
|
+
if (!event || !event.type) return;
|
|
162
|
+
|
|
163
|
+
// Update action registry from available_actions events (before filtering)
|
|
164
|
+
if (event.type === 'available_actions' && event.data?.by_service) {
|
|
165
|
+
this.updateActionRegistry(event.data.by_service);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Process via onFeedUpdate which handles filtering and dedup
|
|
169
|
+
this.onFeedUpdate(event);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Load initial task from API (for existing sessions)
|
|
174
|
+
*/
|
|
175
|
+
async loadInitialTask() {
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(`/api/session/${this.sessionId}/task`);
|
|
178
|
+
if (response.ok) {
|
|
179
|
+
const data = await response.json();
|
|
180
|
+
if (data.status === 'ok' && data.task) {
|
|
181
|
+
const timestamp = data.timestamp || (Date.now() / 1000);
|
|
182
|
+
this.addMessage(this._createMessage('user', data.task, {
|
|
183
|
+
id: 'msg_task_initial',
|
|
184
|
+
timestamp,
|
|
185
|
+
metadata: { isInitialTask: true }
|
|
186
|
+
}), { skipScroll: true });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logger.warn('[Chat] Failed to load initial task:', err);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check session ownership and set up read-only mode for non-owners
|
|
196
|
+
*/
|
|
197
|
+
checkOwnershipAndSetupReadOnly() {
|
|
198
|
+
// Use centralized session state
|
|
199
|
+
const isOwner = sessionState.isOwner;
|
|
200
|
+
const isAuthenticated = sessionState.isAuthenticated;
|
|
201
|
+
|
|
202
|
+
logger.debug('[Chat] Ownership check:', { isOwner, isAuthenticated, isNew: this.isNewSession });
|
|
203
|
+
|
|
204
|
+
// If user is not the owner, disable input and show notice
|
|
205
|
+
if (!isOwner && !this.isNewSession) {
|
|
206
|
+
this.setReadOnlyMode(isAuthenticated);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Enable read-only mode (disable inputs for non-owners)
|
|
212
|
+
*/
|
|
213
|
+
setReadOnlyMode(isAuthenticated) {
|
|
214
|
+
logger.debug('[Chat] Setting read-only mode');
|
|
215
|
+
|
|
216
|
+
// Hide the entire input wrapper
|
|
217
|
+
const inputWrapper = document.querySelector('.chat-input-wrapper');
|
|
218
|
+
if (inputWrapper) {
|
|
219
|
+
inputWrapper.style.display = "none";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Also hide attached files container and upload progress
|
|
223
|
+
const attachedFilesContainer = document.getElementById('attached-files-container');
|
|
224
|
+
if (attachedFilesContainer) {
|
|
225
|
+
attachedFilesContainer.style.display = "none";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const uploadProgress = document.getElementById('upload-progress');
|
|
229
|
+
if (uploadProgress) {
|
|
230
|
+
uploadProgress.style.display = "none";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Show read-only notice (will overlay the hidden input area)
|
|
234
|
+
const readonlyNotice = document.getElementById('readonly-notice');
|
|
235
|
+
const readonlyNoticeText = document.getElementById('readonly-notice-text');
|
|
236
|
+
|
|
237
|
+
if (readonlyNotice && readonlyNoticeText) {
|
|
238
|
+
if (isAuthenticated) {
|
|
239
|
+
readonlyNoticeText.textContent = "šŗ Viewing session in read-only mode. Only the session owner can send messages.";
|
|
240
|
+
} else {
|
|
241
|
+
readonlyNoticeText.innerHTML = 'š <a href="/signin" style="color: #ffc107; text-decoration: underline;">Sign in</a> to interact with this session.';
|
|
242
|
+
}
|
|
243
|
+
readonlyNotice.style.display = 'block';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async connectToSocket() {
|
|
248
|
+
try {
|
|
249
|
+
if (window.socketReady) {
|
|
250
|
+
logger.debug('[Chat] Waiting for socket...');
|
|
251
|
+
this.socket = await window.socketReady;
|
|
252
|
+
this.setupSocketListeners();
|
|
253
|
+
this.reconnectBackoff.reset(); // Reset on successful connection
|
|
254
|
+
logger.debug('[Chat] Socket connected');
|
|
255
|
+
} else {
|
|
256
|
+
// Use exponential backoff for retries
|
|
257
|
+
const delay = this.reconnectBackoff.nextDelay();
|
|
258
|
+
if (delay === null) {
|
|
259
|
+
logger.error('[Chat] Max reconnection attempts reached');
|
|
260
|
+
this.showError('Unable to connect. Please refresh the page.');
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
logger.warn(`[Chat] Socket not available, retrying in ${delay}ms`);
|
|
264
|
+
setTimeout(() => this.connectToSocket(), delay);
|
|
265
|
+
}
|
|
266
|
+
} catch (err) {
|
|
267
|
+
logger.error('[Chat] Socket connection error:', err);
|
|
268
|
+
// Use backoff for error retries too
|
|
269
|
+
const delay = this.reconnectBackoff.nextDelay();
|
|
270
|
+
if (delay === null) {
|
|
271
|
+
this.showError('Connection failed. Please refresh the page.');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.showError(`Connection error, retrying in ${Math.round(delay/1000)}s...`);
|
|
275
|
+
setTimeout(() => this.connectToSocket(), delay);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Set up custom event listeners (always ready, even before socket)
|
|
281
|
+
*/
|
|
282
|
+
setupCustomEventListeners() {
|
|
283
|
+
logger.debug('[Chat] Setting up custom event listeners');
|
|
284
|
+
|
|
285
|
+
// NOTE: Feed updates come via EventStore subscription (subscribeToEventStore)
|
|
286
|
+
// The chat:feedUpdate event bus is NO LONGER used to avoid duplicate processing
|
|
287
|
+
|
|
288
|
+
// Listen for tab activation events - save handler for cleanup
|
|
289
|
+
this._boundHandlers.tabActivated = (event) => {
|
|
290
|
+
if (event.detail.tabId === 'chat-tab') {
|
|
291
|
+
logger.debug('[Chat] Tab activated - scrolling to bottom');
|
|
292
|
+
this.scrollToBottom();
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
on('tab:activated', this._boundHandlers.tabActivated);
|
|
296
|
+
|
|
297
|
+
logger.debug('[Chat] ā
Custom event listeners ready');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// === UTILITY METHODS ===
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Format timestamp to HH:MM:SS time string
|
|
304
|
+
*/
|
|
305
|
+
_formatTime(timestamp) {
|
|
306
|
+
return new Date(timestamp * 1000).toLocaleTimeString('en-US', {
|
|
307
|
+
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Render thinking indicator HTML
|
|
313
|
+
*/
|
|
314
|
+
_renderThinkingIndicator(withEscHint = false) {
|
|
315
|
+
const base = '<span class="thinking-text">thinking</span><span class="thinking-dots"><span>.</span><span>.</span><span>.</span></span>';
|
|
316
|
+
return withEscHint ? `${base} <span class="esc-hint">(esc to interrupt)</span>` : base;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Render message header (time + author)
|
|
321
|
+
*/
|
|
322
|
+
_renderMessageHeader(time, author = 'agent') {
|
|
323
|
+
return `<span class="message-time">[${time}]</span><span class="message-author"><${author}></span>`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Create a message object with standard structure
|
|
328
|
+
*/
|
|
329
|
+
_createMessage(type, text, overrides = {}) {
|
|
330
|
+
return {
|
|
331
|
+
id: `msg_${type}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
332
|
+
type,
|
|
333
|
+
text,
|
|
334
|
+
timestamp: Date.now() / 1000,
|
|
335
|
+
...overrides
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Reset current agent message state
|
|
341
|
+
*/
|
|
342
|
+
_resetAgentMessageState() {
|
|
343
|
+
this.currentAgentMessage = null;
|
|
344
|
+
this.currentAgentMessageId = null;
|
|
345
|
+
this.isStreaming = false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Clean up orphan thinking indicators from DOM
|
|
350
|
+
*/
|
|
351
|
+
_cleanupOrphanThinkingIndicators() {
|
|
352
|
+
const orphans = document.querySelectorAll('.step-thinking-dots, .agent-message .thinking-dots');
|
|
353
|
+
if (orphans.length > 0) {
|
|
354
|
+
logger.debug('[Chat] Cleaning up', orphans.length, 'orphan thinking indicators');
|
|
355
|
+
orphans.forEach(el => el.remove());
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Update action registry from available_actions event
|
|
361
|
+
*/
|
|
362
|
+
updateActionRegistry(byService) {
|
|
363
|
+
logger.debug('[Chat] Updating action registry from available_actions');
|
|
364
|
+
|
|
365
|
+
// byService format: { service_name: [action1, action2, ...] }
|
|
366
|
+
for (const [serviceName, actions] of Object.entries(byService)) {
|
|
367
|
+
if (Array.isArray(actions)) {
|
|
368
|
+
actions.forEach(action => {
|
|
369
|
+
// Handle both string and object formats
|
|
370
|
+
const actionName = typeof action === 'string' ? action : action.name || action.action_type;
|
|
371
|
+
if (actionName) {
|
|
372
|
+
this.actionRegistry[actionName.toLowerCase()] = serviceName.toLowerCase();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
logger.debug('[Chat] Action registry updated:', Object.keys(this.actionRegistry).length, 'actions');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Set up socket-specific listeners (streaming)
|
|
383
|
+
*/
|
|
384
|
+
setupSocketListeners() {
|
|
385
|
+
logger.debug('[Chat] Setting up socket listeners for session:', this.sessionId);
|
|
386
|
+
|
|
387
|
+
if (!this.socket) {
|
|
388
|
+
logger.warn('[Chat] No socket available for listeners');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Streaming chunks (live updates during agent thinking)
|
|
393
|
+
this.socket.on('stream_chunk', (data) => {
|
|
394
|
+
logger.debug('[Chat] Received stream_chunk for session:', data.session_id);
|
|
395
|
+
|
|
396
|
+
if (data.session_id === this.sessionId) {
|
|
397
|
+
this.onStreamChunk(data);
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Also listen for 'streaming_output' (alternative event name)
|
|
402
|
+
this.socket.on('streaming_output', (data) => {
|
|
403
|
+
logger.debug('[Chat] Received streaming_output for session:', data.session_id);
|
|
404
|
+
|
|
405
|
+
if (data.session_id === this.sessionId) {
|
|
406
|
+
this.onStreamChunk(data);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Also listen for 'stream_update' (emitted by SSE-to-WS bridge)
|
|
411
|
+
this.socket.on('stream_update', (data) => {
|
|
412
|
+
logger.debug('[Chat] Received stream_update for session:', data.session_id);
|
|
413
|
+
|
|
414
|
+
if (data.session_id === this.sessionId) {
|
|
415
|
+
this.onStreamChunk(data);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
logger.debug('[Chat] Socket listeners ready');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Check if chat tab is currently active
|
|
424
|
+
*/
|
|
425
|
+
isChatTabActive() {
|
|
426
|
+
const chatTab = document.getElementById('chat-tab');
|
|
427
|
+
return chatTab && chatTab.classList.contains('active');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
setupUIListeners() {
|
|
431
|
+
// Store bound handlers for cleanup
|
|
432
|
+
|
|
433
|
+
// Send button
|
|
434
|
+
this._boundHandlers.sendClick = () => this.sendMessage();
|
|
435
|
+
this.sendButton.addEventListener('click', this._boundHandlers.sendClick);
|
|
436
|
+
|
|
437
|
+
// Enter key
|
|
438
|
+
this._boundHandlers.inputKeydown = (e) => {
|
|
439
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
this.sendMessage();
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
this.inputField.addEventListener('keydown', this._boundHandlers.inputKeydown);
|
|
445
|
+
|
|
446
|
+
// Clipboard paste support for images
|
|
447
|
+
this._boundHandlers.inputPaste = async (e) => {
|
|
448
|
+
const items = e.clipboardData?.items;
|
|
449
|
+
if (!items) return;
|
|
450
|
+
|
|
451
|
+
logger.debug('[Chat] Paste event detected, checking for images...');
|
|
452
|
+
|
|
453
|
+
for (const item of items) {
|
|
454
|
+
if (item.type.startsWith('image/')) {
|
|
455
|
+
e.preventDefault(); // Prevent default text paste
|
|
456
|
+
|
|
457
|
+
logger.debug('[Chat] Image pasted from clipboard:', item.type);
|
|
458
|
+
|
|
459
|
+
// Get image file from clipboard
|
|
460
|
+
const imageFile = item.getAsFile();
|
|
461
|
+
if (imageFile && this.fileManager) {
|
|
462
|
+
try {
|
|
463
|
+
this.fileManager.addFile(imageFile);
|
|
464
|
+
logger.debug('[Chat] ā Pasted image added to attachments');
|
|
465
|
+
} catch (error) {
|
|
466
|
+
logger.error('[Chat] ā Failed to add pasted image:', error);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
this.inputField.addEventListener('paste', this._boundHandlers.inputPaste);
|
|
473
|
+
|
|
474
|
+
// Auto-resize textarea as user types
|
|
475
|
+
this._boundHandlers.inputChange = () => this.autoResizeTextarea();
|
|
476
|
+
this.inputField.addEventListener('input', this._boundHandlers.inputChange);
|
|
477
|
+
|
|
478
|
+
// Scroll detection (throttled for performance)
|
|
479
|
+
this._boundHandlers.scrollThrottled = rafThrottle(() => this.onScroll());
|
|
480
|
+
this.chatContainer.addEventListener('scroll', this._boundHandlers.scrollThrottled);
|
|
481
|
+
|
|
482
|
+
// Scroll to bottom button
|
|
483
|
+
if (this.scrollButton) {
|
|
484
|
+
this._boundHandlers.scrollButtonClick = () => this.scrollToBottom(true);
|
|
485
|
+
this.scrollButton.addEventListener('click', this._boundHandlers.scrollButtonClick);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Action expand/collapse (event delegation on chat container)
|
|
489
|
+
this._boundHandlers.containerClick = (e) => {
|
|
490
|
+
// Find the clicked action line (might be nested in spans)
|
|
491
|
+
const actionLine = e.target.closest('.step-action-line');
|
|
492
|
+
if (actionLine) {
|
|
493
|
+
this.toggleActionDetails(actionLine);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
this.chatContainer.addEventListener('click', this._boundHandlers.containerClick);
|
|
497
|
+
|
|
498
|
+
// ESC key listener for cancel (interrupt)
|
|
499
|
+
// Note: R key for resume removed - sessions auto-resume on new message
|
|
500
|
+
this._boundHandlers.documentKeydown = (e) => {
|
|
501
|
+
if (e.key === 'Escape') {
|
|
502
|
+
this.handleEscapeKey(e);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
document.addEventListener('keydown', this._boundHandlers.documentKeydown);
|
|
506
|
+
|
|
507
|
+
// Mobile keyboard handling - adjust input position when keyboard appears
|
|
508
|
+
this.setupMobileKeyboardHandling();
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
setupMobileKeyboardHandling() {
|
|
512
|
+
// Only set up keyboard handling if visualViewport API is available (modern mobile browsers)
|
|
513
|
+
if ('visualViewport' in window) {
|
|
514
|
+
const chatInputContainer = document.querySelector('.chat-input-container');
|
|
515
|
+
if (!chatInputContainer) return;
|
|
516
|
+
|
|
517
|
+
// Track the original bottom position
|
|
518
|
+
let originalBottom = null;
|
|
519
|
+
|
|
520
|
+
// Store bound handler for cleanup
|
|
521
|
+
this._boundHandlers.viewportResize = () => {
|
|
522
|
+
// Calculate how much the keyboard is taking up
|
|
523
|
+
const viewportHeight = window.visualViewport.height;
|
|
524
|
+
const windowHeight = window.innerHeight;
|
|
525
|
+
const keyboardHeight = windowHeight - viewportHeight;
|
|
526
|
+
|
|
527
|
+
if (keyboardHeight > 0) {
|
|
528
|
+
// Keyboard is visible - move input up
|
|
529
|
+
if (originalBottom === null) {
|
|
530
|
+
originalBottom = chatInputContainer.style.bottom || '0';
|
|
531
|
+
}
|
|
532
|
+
chatInputContainer.style.bottom = `${keyboardHeight}px`;
|
|
533
|
+
chatInputContainer.style.position = 'fixed';
|
|
534
|
+
} else {
|
|
535
|
+
// Keyboard is hidden - restore original position
|
|
536
|
+
if (originalBottom !== null) {
|
|
537
|
+
chatInputContainer.style.bottom = originalBottom;
|
|
538
|
+
originalBottom = null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
window.visualViewport.addEventListener('resize', this._boundHandlers.viewportResize);
|
|
543
|
+
|
|
544
|
+
logger.debug('[Chat] Mobile keyboard handling initialized');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Auto-resize textarea based on content
|
|
550
|
+
*/
|
|
551
|
+
autoResizeTextarea() {
|
|
552
|
+
if (!this.inputField) return;
|
|
553
|
+
|
|
554
|
+
// Reset height to minimum to get accurate scrollHeight
|
|
555
|
+
this.inputField.style.height = '28px';
|
|
556
|
+
|
|
557
|
+
// Calculate new height based on content
|
|
558
|
+
const scrollHeight = this.inputField.scrollHeight;
|
|
559
|
+
|
|
560
|
+
// Set height to content height (CSS max-height will constrain it)
|
|
561
|
+
this.inputField.style.height = `${scrollHeight}px`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Toggle action details expansion
|
|
566
|
+
*/
|
|
567
|
+
toggleActionDetails(actionLine) {
|
|
568
|
+
const actionId = actionLine.getAttribute('data-action-id');
|
|
569
|
+
if (!actionId) return;
|
|
570
|
+
|
|
571
|
+
const detailsEl = document.getElementById(actionId);
|
|
572
|
+
if (!detailsEl) return;
|
|
573
|
+
|
|
574
|
+
// Toggle expanded class
|
|
575
|
+
if (detailsEl.classList.contains('expanded')) {
|
|
576
|
+
detailsEl.classList.remove('expanded');
|
|
577
|
+
} else {
|
|
578
|
+
detailsEl.classList.add('expanded');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Handle ESC key press - cancel/interrupt agent
|
|
584
|
+
* Note: Pause functionality removed. ESC now cancels the session.
|
|
585
|
+
* To continue, user sends a new message which auto-resumes.
|
|
586
|
+
*/
|
|
587
|
+
async handleEscapeKey(event) {
|
|
588
|
+
// Only handle if we're in chat tab and session is running
|
|
589
|
+
if (!this.isChatTabActive()) {
|
|
590
|
+
logger.debug('[Chat] ESC pressed but chat tab not active');
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
logger.debug('[Chat] ESC pressed - sessionStatus:', this.sessionStatus, 'isNewSession:', this.isNewSession);
|
|
595
|
+
|
|
596
|
+
if (this.sessionStatus === 'running' && !this.isNewSession) {
|
|
597
|
+
event.preventDefault();
|
|
598
|
+
logger.debug('[Chat] ā
ESC CANCELLING SESSION');
|
|
599
|
+
await this.cancelSession();
|
|
600
|
+
} else {
|
|
601
|
+
logger.debug('[Chat] āļø ESC ignored - not running or new session');
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Cancel the current session via API
|
|
607
|
+
* Note: This replaces the old pauseSession method.
|
|
608
|
+
* Users can send a new message to continue with modifications.
|
|
609
|
+
*/
|
|
610
|
+
async cancelSession() {
|
|
611
|
+
const originalStatus = this.sessionStatus;
|
|
612
|
+
|
|
613
|
+
try {
|
|
614
|
+
logger.debug('[Chat] š Calling cancel API for session:', this.sessionId);
|
|
615
|
+
|
|
616
|
+
// Optimistic update
|
|
617
|
+
this.sessionStatus = 'cancelled';
|
|
618
|
+
|
|
619
|
+
const response = await fetch(`/api/task/sessions/${this.sessionId}/cancel`, {
|
|
620
|
+
method: 'POST',
|
|
621
|
+
headers: { 'Content-Type': 'application/json' }
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
logger.debug('[Chat] Cancel API response status:', response.status);
|
|
625
|
+
|
|
626
|
+
if (!response.ok) {
|
|
627
|
+
// Parse error response for better message
|
|
628
|
+
let errorMsg = `HTTP ${response.status}`;
|
|
629
|
+
try {
|
|
630
|
+
const errorData = await response.json();
|
|
631
|
+
errorMsg = errorData.detail || errorData.message || errorMsg;
|
|
632
|
+
} catch (e) {
|
|
633
|
+
// If not JSON, try text
|
|
634
|
+
try {
|
|
635
|
+
errorMsg = await response.text() || errorMsg;
|
|
636
|
+
} catch (e2) {}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (response.status === 404) {
|
|
640
|
+
throw new Error('Session not found');
|
|
641
|
+
} else if (response.status === 400) {
|
|
642
|
+
throw new Error(errorMsg || 'Cannot cancel: session may already be stopped');
|
|
643
|
+
}
|
|
644
|
+
throw new Error(errorMsg);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Show system message
|
|
648
|
+
this.addMessage(this._createMessage('system', 'ā¹ļø Session cancelled. Send a message to continue.'));
|
|
649
|
+
|
|
650
|
+
logger.debug('[Chat] ā
Session cancelled successfully');
|
|
651
|
+
|
|
652
|
+
} catch (error) {
|
|
653
|
+
// Rollback on failure
|
|
654
|
+
this.sessionStatus = originalStatus;
|
|
655
|
+
logger.error('[Chat] ā Error cancelling session:', error);
|
|
656
|
+
this.showError('Failed to cancel session: ' + error.message);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Create immediate thinking message when user sends a message
|
|
662
|
+
* Shows <agent> thinking: š§ instantly
|
|
663
|
+
*
|
|
664
|
+
* MESSAGE FORMAT v2: Uses hierarchical steps structure
|
|
665
|
+
* - steps: Array of {stepNumber, timestamp, thinking, actions, status}
|
|
666
|
+
* - Each thinking update creates a new step
|
|
667
|
+
* - Actions are grouped within their step
|
|
668
|
+
*/
|
|
669
|
+
createImmediateThinkingMessage() {
|
|
670
|
+
// Simplified for chunk-based approach:
|
|
671
|
+
// Just mark session as running and prepare for new chunks
|
|
672
|
+
logger.debug('[Chat] š§ Session marked as running, ready for chunks');
|
|
673
|
+
|
|
674
|
+
// Mark session as running so ESC can interrupt
|
|
675
|
+
this.sessionStatus = 'running';
|
|
676
|
+
|
|
677
|
+
// Start new iteration group if not already tracking
|
|
678
|
+
if (this._currentIteration === 0) {
|
|
679
|
+
this._currentIteration = 1;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Legacy cleanup
|
|
683
|
+
if (this.currentAgentMessage) {
|
|
684
|
+
this._resetAgentMessageState();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
this.isStreaming = false;
|
|
688
|
+
logger.debug('[Chat] ā
Session ready for chunks, iteration:', this._currentIteration);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Clean up thinking state on cancellation or error.
|
|
693
|
+
* Preserves the thinking message with a "cancelled" visual state instead of removing it.
|
|
694
|
+
* This keeps visibility into what the agent was doing before interruption.
|
|
695
|
+
*/
|
|
696
|
+
cleanupThinkingState() {
|
|
697
|
+
logger.debug('[Chat] Cleaning up thinking state');
|
|
698
|
+
|
|
699
|
+
// If there's a current thinking message, mark it as cancelled (don't remove)
|
|
700
|
+
if (this.currentAgentMessage && this.currentAgentMessage.status === 'thinking') {
|
|
701
|
+
// Mark as cancelled in state
|
|
702
|
+
this.currentAgentMessage.status = 'cancelled';
|
|
703
|
+
|
|
704
|
+
// Update the DOM to show cancelled state
|
|
705
|
+
const messageElement = this.chatContainer?.querySelector(`[data-message-id="${this.currentAgentMessage.id}"]`);
|
|
706
|
+
if (messageElement) {
|
|
707
|
+
// Remove thinking indicators
|
|
708
|
+
const thinkingIndicators = messageElement.querySelectorAll('.thinking-dots, .step-thinking-dots');
|
|
709
|
+
thinkingIndicators.forEach(el => el.remove());
|
|
710
|
+
|
|
711
|
+
// Add cancelled indicator if there are iterations to show
|
|
712
|
+
if (this.currentAgentMessage.iterations?.length > 0) {
|
|
713
|
+
// Re-render with cancelled status
|
|
714
|
+
messageElement.innerHTML = this.renderAgentMessage(this.currentAgentMessage);
|
|
715
|
+
messageElement.classList.add('message-cancelled');
|
|
716
|
+
} else {
|
|
717
|
+
// No meaningful content - remove empty message
|
|
718
|
+
messageElement.remove();
|
|
719
|
+
const idx = this.messages.findIndex(m => m.id === this.currentAgentMessage.id);
|
|
720
|
+
if (idx !== -1) {
|
|
721
|
+
this.messages.splice(idx, 1);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Reset state (but message remains visible if it had content)
|
|
728
|
+
this._resetAgentMessageState();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Load chat history - delegates to EventStore subscription
|
|
734
|
+
* @deprecated Use subscribeToEventStore() directly. This exists for backwards compatibility.
|
|
735
|
+
*/
|
|
736
|
+
async loadHistory() {
|
|
737
|
+
logger.debug('[Chat] loadHistory() called - delegating to EventStore subscription');
|
|
738
|
+
|
|
739
|
+
// Subscribe to EventStore if not already subscribed
|
|
740
|
+
if (!this._eventStoreUnsubscribe) {
|
|
741
|
+
this.subscribeToEventStore();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Load initial task
|
|
745
|
+
await this.loadInitialTask();
|
|
746
|
+
|
|
747
|
+
// Scroll to bottom
|
|
748
|
+
this.scrollToBottom();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
onFeedUpdate(feedEvent, options = {}) {
|
|
752
|
+
logger.debug('[Chat] š„ Processing event:', feedEvent.type, feedEvent);
|
|
753
|
+
|
|
754
|
+
// Skip filtered events (for events coming via chat:feedUpdate event bus)
|
|
755
|
+
if (!chatFilter.shouldShow(feedEvent)) {
|
|
756
|
+
logger.debug('[Chat] āļø Event filtered:', feedEvent.type);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Skip duplicates (using _id for deduplication)
|
|
761
|
+
const eventId = feedEvent._id || `${feedEvent.type}_${feedEvent.timestamp || Date.now()}`;
|
|
762
|
+
if (this._processedEventIds.has(eventId)) {
|
|
763
|
+
logger.debug('[Chat] āļø Event duplicate:', feedEvent.type);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Mark as processed
|
|
768
|
+
this._processedEventIds.add(eventId);
|
|
769
|
+
|
|
770
|
+
logger.debug('[Chat] ā
Event passed filters, processing:', feedEvent.type);
|
|
771
|
+
|
|
772
|
+
// Route events to appropriate handlers
|
|
773
|
+
const eventType = feedEvent.type;
|
|
774
|
+
|
|
775
|
+
switch (eventType) {
|
|
776
|
+
// User messages
|
|
777
|
+
case 'user_message':
|
|
778
|
+
this.handleUserMessage(feedEvent);
|
|
779
|
+
break;
|
|
780
|
+
|
|
781
|
+
// Agent thinking/step events ā update current thinking message
|
|
782
|
+
case 'step':
|
|
783
|
+
case 'agent_step':
|
|
784
|
+
case 'planner':
|
|
785
|
+
case 'evaluation':
|
|
786
|
+
case 'task_progress':
|
|
787
|
+
this.handleThinkingUpdate(feedEvent);
|
|
788
|
+
break;
|
|
789
|
+
|
|
790
|
+
// Final agent output ā complete the thinking message
|
|
791
|
+
case 'agent_message':
|
|
792
|
+
case 'final_message':
|
|
793
|
+
case 'result':
|
|
794
|
+
this.handleFinalMessage(feedEvent);
|
|
795
|
+
break;
|
|
796
|
+
|
|
797
|
+
// Session/system status events
|
|
798
|
+
case 'session_halted':
|
|
799
|
+
case 'session_done':
|
|
800
|
+
case 'task_complete':
|
|
801
|
+
this.handleFinalMessage(feedEvent); // These also contain final text
|
|
802
|
+
break;
|
|
803
|
+
|
|
804
|
+
// CRITICAL: Handle status events to keep sessionStatus synced
|
|
805
|
+
case 'status':
|
|
806
|
+
this.handleStatusEvent(feedEvent);
|
|
807
|
+
break;
|
|
808
|
+
|
|
809
|
+
case 'status_update':
|
|
810
|
+
case 'session_start':
|
|
811
|
+
case 'session_completion':
|
|
812
|
+
this.handleSystemMessage(feedEvent);
|
|
813
|
+
break;
|
|
814
|
+
|
|
815
|
+
// Technical events (filtered in addMessage, but handle gracefully)
|
|
816
|
+
case 'llm_request':
|
|
817
|
+
case 'tool_execution':
|
|
818
|
+
// Skip - these clutter the chat
|
|
819
|
+
break;
|
|
820
|
+
|
|
821
|
+
case 'error':
|
|
822
|
+
this.handleErrorEvent(feedEvent);
|
|
823
|
+
break;
|
|
824
|
+
|
|
825
|
+
case 'multi_agent_relationship':
|
|
826
|
+
this.handleMultiAgentRelationship(feedEvent);
|
|
827
|
+
break;
|
|
828
|
+
|
|
829
|
+
case 'available_actions':
|
|
830
|
+
// Already handled by updateActionRegistry()
|
|
831
|
+
break;
|
|
832
|
+
|
|
833
|
+
// Queue status updates for the queue indicator
|
|
834
|
+
case 'queue_status':
|
|
835
|
+
this.handleQueueStatus(feedEvent);
|
|
836
|
+
break;
|
|
837
|
+
|
|
838
|
+
// Session cancelled event from backend (paused/resumed removed)
|
|
839
|
+
case 'session_cancelled':
|
|
840
|
+
this.handleSessionCancelled(feedEvent);
|
|
841
|
+
break;
|
|
842
|
+
|
|
843
|
+
// DEPRECATED: These events are no longer emitted but kept for backwards compatibility
|
|
844
|
+
case 'session_paused':
|
|
845
|
+
// Treat paused as cancelled for backwards compatibility
|
|
846
|
+
this.handleSessionCancelled(feedEvent);
|
|
847
|
+
break;
|
|
848
|
+
|
|
849
|
+
case 'session_resumed':
|
|
850
|
+
// Sessions now auto-resume on new message - ignore this event
|
|
851
|
+
logger.debug('[Chat] Ignoring deprecated session_resumed event');
|
|
852
|
+
break;
|
|
853
|
+
|
|
854
|
+
// Iteration complete - marks end of an iteration with file info
|
|
855
|
+
case 'iteration_complete':
|
|
856
|
+
this.handleIterationComplete(feedEvent);
|
|
857
|
+
break;
|
|
858
|
+
|
|
859
|
+
default:
|
|
860
|
+
// Unknown events - check if they have thinking data
|
|
861
|
+
const data = feedEvent.data || {};
|
|
862
|
+
if (data.reasoning || data.thought || data.task_progress || data.actions || data.plan) {
|
|
863
|
+
this.handleThinkingUpdate(feedEvent);
|
|
864
|
+
}
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
handleUserMessage(event) {
|
|
870
|
+
const data = event.data || {};
|
|
871
|
+
const messageText = data.text || data.message || '';
|
|
872
|
+
|
|
873
|
+
// Remove from pending queue (feed event confirms it was received)
|
|
874
|
+
this._removeFromPendingQueue(messageText);
|
|
875
|
+
|
|
876
|
+
// FIX: Check if a user message with the same text already exists (prevents duplicates)
|
|
877
|
+
// This happens when loadHistory() adds the initial task, then feed events also contain it
|
|
878
|
+
const normalizedText = messageText.trim().toLowerCase();
|
|
879
|
+
const isDuplicate = this.messages.some(msg =>
|
|
880
|
+
msg.type === 'user' &&
|
|
881
|
+
(msg.text || msg.content || '').trim().toLowerCase() === normalizedText
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
if (isDuplicate) {
|
|
885
|
+
logger.debug('[Chat] āļø Skipping duplicate user message:', messageText.substring(0, 50));
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
logger.debug('[Chat] š¤ User message from backend:', messageText.substring(0, 50));
|
|
890
|
+
|
|
891
|
+
// Start a new iteration group for the upcoming agent response
|
|
892
|
+
this._currentIteration++;
|
|
893
|
+
logger.debug('[Chat] š New iteration group:', this._currentIteration);
|
|
894
|
+
|
|
895
|
+
// Trigger auto-collapse of previous groups
|
|
896
|
+
this._checkAutoCollapse();
|
|
897
|
+
|
|
898
|
+
// Add the user message with proper seq for ordering
|
|
899
|
+
const userMsg = this._createMessage('user', messageText, {
|
|
900
|
+
id: `msg_user_${event._id || event.timestamp}`,
|
|
901
|
+
timestamp: event._ts_ms ? event._ts_ms / 1000 : (event.timestamp || Date.now() / 1000),
|
|
902
|
+
seq: event._seq || (event._ts_ms || Date.now()) // Use _seq for ordering
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
this.addMessage(userMsg);
|
|
906
|
+
|
|
907
|
+
// Mark session as running for new task
|
|
908
|
+
this.sessionStatus = 'running';
|
|
909
|
+
|
|
910
|
+
// Legacy cleanup (for backwards compatibility)
|
|
911
|
+
if (this.currentAgentMessage) {
|
|
912
|
+
this._resetAgentMessageState();
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Handle status events from backend to keep sessionStatus synced
|
|
918
|
+
* This is CRITICAL for proper thinking indicator behavior on session continuation
|
|
919
|
+
*/
|
|
920
|
+
handleStatusEvent(event) {
|
|
921
|
+
const data = event.data || {};
|
|
922
|
+
const newStatus = (data.status || '').toLowerCase();
|
|
923
|
+
|
|
924
|
+
logger.debug('[Chat] š Status event received:', newStatus, 'current:', this.sessionStatus);
|
|
925
|
+
|
|
926
|
+
if (!newStatus) return;
|
|
927
|
+
|
|
928
|
+
// Update sessionStatus based on backend status
|
|
929
|
+
const previousStatus = this.sessionStatus;
|
|
930
|
+
|
|
931
|
+
if (newStatus === 'running' || newStatus === 'active' || newStatus === 'resumed') {
|
|
932
|
+
this.sessionStatus = 'running';
|
|
933
|
+
|
|
934
|
+
// If transitioning TO running and we don't have a thinking message, create one
|
|
935
|
+
// This handles the case where session resumes from backend (after completed or cancelled)
|
|
936
|
+
if ((previousStatus === 'completed' || previousStatus === 'cancelled') && !this.currentAgentMessage) {
|
|
937
|
+
logger.debug('[Chat] š Session resumed from', previousStatus, ', creating thinking message');
|
|
938
|
+
this.createImmediateThinkingMessage();
|
|
939
|
+
}
|
|
940
|
+
} else if (newStatus === 'completed' || newStatus === 'done' || newStatus === 'success') {
|
|
941
|
+
this.sessionStatus = 'completed';
|
|
942
|
+
|
|
943
|
+
// FIX: When status changes to completed, ensure all thinking indicators are removed
|
|
944
|
+
// This handles cases where handleFinalMessage might have missed them
|
|
945
|
+
if (this.currentAgentMessage && this.currentAgentMessage.status === 'thinking') {
|
|
946
|
+
logger.debug('[Chat] š Status event forcing completion of current message');
|
|
947
|
+
this.currentAgentMessage.status = 'completed';
|
|
948
|
+
|
|
949
|
+
const msgEl = document.querySelector(`[data-message-id="${this.currentAgentMessage.id}"]`);
|
|
950
|
+
if (msgEl) {
|
|
951
|
+
msgEl.innerHTML = this.renderAgentMessage(this.currentAgentMessage);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
this.currentAgentMessage = null;
|
|
955
|
+
this.currentAgentMessageId = null;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Also clean up any orphan thinking indicators
|
|
959
|
+
this._cleanupOrphanThinkingIndicators();
|
|
960
|
+
} else if (newStatus === 'cancelled' || newStatus === 'suspended') {
|
|
961
|
+
// Note: 'paused' status removed - use 'cancelled' for user interruption
|
|
962
|
+
this.sessionStatus = 'cancelled';
|
|
963
|
+
this.cleanupThinkingState();
|
|
964
|
+
} else if (newStatus === 'error' || newStatus === 'failed') {
|
|
965
|
+
this.sessionStatus = 'error';
|
|
966
|
+
// Clean up any pending thinking state on error
|
|
967
|
+
this.cleanupThinkingState();
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
logger.debug('[Chat] š sessionStatus updated:', previousStatus, 'ā', this.sessionStatus);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Handle queue status updates to show queue indicator in UI
|
|
975
|
+
* Shows pending messages as a compact list
|
|
976
|
+
*/
|
|
977
|
+
handleQueueStatus(event) {
|
|
978
|
+
const data = event.data || {};
|
|
979
|
+
const queuedCount = data.queued_count || 0;
|
|
980
|
+
const processing = data.processing || false;
|
|
981
|
+
|
|
982
|
+
logger.debug('[Chat] šØ Queue status update:', queuedCount, 'queued, processing:', processing);
|
|
983
|
+
|
|
984
|
+
// Update queue indicator with our pending messages
|
|
985
|
+
this._updateQueueIndicator(processing);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Add a message to the pending queue display
|
|
990
|
+
* Shows immediately in UI before feed event confirms it
|
|
991
|
+
*/
|
|
992
|
+
_addToPendingQueue(text) {
|
|
993
|
+
const pendingMsg = {
|
|
994
|
+
id: `pending_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
995
|
+
text: text,
|
|
996
|
+
timestamp: Date.now()
|
|
997
|
+
};
|
|
998
|
+
this.pendingMessages.push(pendingMsg);
|
|
999
|
+
logger.debug('[Chat] Added to pending queue:', pendingMsg.id, 'total:', this.pendingMessages.length);
|
|
1000
|
+
this._updateQueueIndicator(false);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Remove a message from pending queue when feed event arrives
|
|
1005
|
+
*/
|
|
1006
|
+
_removeFromPendingQueue(text) {
|
|
1007
|
+
const normalizedText = (text || '').trim().toLowerCase();
|
|
1008
|
+
const idx = this.pendingMessages.findIndex(m =>
|
|
1009
|
+
(m.text || '').trim().toLowerCase() === normalizedText
|
|
1010
|
+
);
|
|
1011
|
+
if (idx !== -1) {
|
|
1012
|
+
const removed = this.pendingMessages.splice(idx, 1)[0];
|
|
1013
|
+
logger.debug('[Chat] Removed from pending queue:', removed.id, 'remaining:', this.pendingMessages.length);
|
|
1014
|
+
this._updateQueueIndicator(false);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Update the queue indicator UI with pending messages
|
|
1020
|
+
* Compact inline display aligned with input area
|
|
1021
|
+
*/
|
|
1022
|
+
_updateQueueIndicator(processing) {
|
|
1023
|
+
let queueIndicator = document.getElementById('chat-queue-indicator');
|
|
1024
|
+
|
|
1025
|
+
const hasPending = this.pendingMessages.length > 0 || processing;
|
|
1026
|
+
|
|
1027
|
+
if (hasPending) {
|
|
1028
|
+
if (!queueIndicator) {
|
|
1029
|
+
// Create queue indicator if it doesn't exist
|
|
1030
|
+
queueIndicator = document.createElement('div');
|
|
1031
|
+
queueIndicator.id = 'chat-queue-indicator';
|
|
1032
|
+
queueIndicator.className = 'queue-indicator';
|
|
1033
|
+
|
|
1034
|
+
// Insert inside input container, before the wrapper
|
|
1035
|
+
const inputContainer = document.querySelector('.chat-input-container');
|
|
1036
|
+
const inputWrapper = document.querySelector('.chat-input-wrapper');
|
|
1037
|
+
if (inputContainer && inputWrapper) {
|
|
1038
|
+
inputContainer.insertBefore(queueIndicator, inputWrapper);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Build compact inline display
|
|
1043
|
+
let html = '<span class="queue-header">';
|
|
1044
|
+
if (processing) {
|
|
1045
|
+
html += '<span class="queue-processing">processing</span>';
|
|
1046
|
+
} else {
|
|
1047
|
+
html += `<span class="queue-pending">${this.pendingMessages.length} queued</span>`;
|
|
1048
|
+
}
|
|
1049
|
+
html += '</span>';
|
|
1050
|
+
|
|
1051
|
+
if (this.pendingMessages.length > 0) {
|
|
1052
|
+
html += '<ul class="queue-list">';
|
|
1053
|
+
for (const msg of this.pendingMessages) {
|
|
1054
|
+
// Truncate long messages
|
|
1055
|
+
const truncated = msg.text.length > 40
|
|
1056
|
+
? msg.text.substring(0, 40) + 'ā¦'
|
|
1057
|
+
: msg.text;
|
|
1058
|
+
html += `<li class="queue-item">${escapeHtml(truncated)}</li>`;
|
|
1059
|
+
}
|
|
1060
|
+
html += '</ul>';
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
queueIndicator.innerHTML = html;
|
|
1064
|
+
queueIndicator.style.display = 'flex';
|
|
1065
|
+
} else {
|
|
1066
|
+
// Hide indicator when queue is empty
|
|
1067
|
+
if (queueIndicator) {
|
|
1068
|
+
queueIndicator.style.display = 'none';
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* Handle session cancelled event from backend feed
|
|
1075
|
+
* Updates sessionStatus and shows UI feedback
|
|
1076
|
+
* Note: This replaces the old handleSessionPaused method
|
|
1077
|
+
*/
|
|
1078
|
+
handleSessionCancelled(event) {
|
|
1079
|
+
const data = event.data || {};
|
|
1080
|
+
logger.debug('[Chat] š„ Received session_cancelled event:', data);
|
|
1081
|
+
|
|
1082
|
+
// Update session status
|
|
1083
|
+
this.sessionStatus = 'cancelled';
|
|
1084
|
+
|
|
1085
|
+
// Clean up any thinking state
|
|
1086
|
+
this.cleanupThinkingState();
|
|
1087
|
+
|
|
1088
|
+
// Only show message if we didn't already show one via API call
|
|
1089
|
+
// Check if the last message is already a cancel message
|
|
1090
|
+
const messages = this.chatContainer?.querySelectorAll('.chat-message');
|
|
1091
|
+
if (messages && messages.length > 0) {
|
|
1092
|
+
const lastMsg = messages[messages.length - 1];
|
|
1093
|
+
const lastText = lastMsg?.textContent || '';
|
|
1094
|
+
if (lastText.includes('Session cancelled')) {
|
|
1095
|
+
logger.debug('[Chat] Skipping duplicate cancel message');
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// Show system message for remote cancel events
|
|
1101
|
+
this.addMessage(this._createMessage('system', 'ā¹ļø Session cancelled. Send a message to continue.', {
|
|
1102
|
+
id: `msg_system_cancel_${event.timestamp}`,
|
|
1103
|
+
timestamp: event.timestamp || (Date.now() / 1000)
|
|
1104
|
+
}));
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
handleSystemMessage(event) {
|
|
1108
|
+
const data = event.data || {};
|
|
1109
|
+
|
|
1110
|
+
// Extract message text from various possible fields
|
|
1111
|
+
let messageText = data.text || data.message || data.status || '';
|
|
1112
|
+
|
|
1113
|
+
// Add context based on event type
|
|
1114
|
+
if (event.type === 'session_halted') {
|
|
1115
|
+
messageText = messageText || 'Session halted';
|
|
1116
|
+
} else if (event.type === 'session_done') {
|
|
1117
|
+
messageText = messageText || 'Session completed';
|
|
1118
|
+
} else if (event.type === 'task_complete') {
|
|
1119
|
+
messageText = messageText || 'Task completed';
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Create system message
|
|
1123
|
+
this.addMessage(this._createMessage('system', messageText, {
|
|
1124
|
+
id: `msg_system_${event.timestamp}`,
|
|
1125
|
+
timestamp: event.timestamp || (Date.now() / 1000)
|
|
1126
|
+
}));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
handleErrorEvent(event) {
|
|
1130
|
+
logger.debug('[Chat] handleErrorEvent:', event);
|
|
1131
|
+
const data = event.data || {};
|
|
1132
|
+
|
|
1133
|
+
// Clean up thinking state on error - stop showing running indicator
|
|
1134
|
+
if (this.currentAgentMessage && this.currentAgentMessage.status === 'thinking') {
|
|
1135
|
+
this.currentAgentMessage.status = 'error';
|
|
1136
|
+
// Re-render to remove thinking indicator
|
|
1137
|
+
const msgEl = document.querySelector(`[data-message-id="${this.currentAgentMessage.id}"]`);
|
|
1138
|
+
if (msgEl) {
|
|
1139
|
+
msgEl.innerHTML = this.renderAgentMessage(this.currentAgentMessage);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const errorMessage = {
|
|
1144
|
+
id: `msg_error_${event.timestamp}`,
|
|
1145
|
+
type: 'error',
|
|
1146
|
+
timestamp: event.timestamp || (Date.now() / 1000),
|
|
1147
|
+
errorType: data.error_type || 'Error',
|
|
1148
|
+
errorMessage: data.error_message || 'Unknown error',
|
|
1149
|
+
errorStack: data.error_stack,
|
|
1150
|
+
recoverable: data.recoverable !== false,
|
|
1151
|
+
context: data.context || {}
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
this.addMessage(errorMessage);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
handleMultiAgentRelationship(event) {
|
|
1158
|
+
logger.debug('[Chat] handleMultiAgentRelationship:', event);
|
|
1159
|
+
const data = event.data || {};
|
|
1160
|
+
|
|
1161
|
+
const relationshipMessage = {
|
|
1162
|
+
id: `msg_relationship_${event.timestamp}`,
|
|
1163
|
+
type: 'multi_agent_relationship',
|
|
1164
|
+
timestamp: event.timestamp || (Date.now() / 1000),
|
|
1165
|
+
agents: data.agent_details || [],
|
|
1166
|
+
orchestratorType: data.orchestrator_type || 'default'
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
this.addMessage(relationshipMessage);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
handleFinalMessage(event) {
|
|
1173
|
+
logger.debug('[Chat] handleFinalMessage called with event:', event);
|
|
1174
|
+
const data = event.data || {};
|
|
1175
|
+
|
|
1176
|
+
// Extract the final message text from various possible fields
|
|
1177
|
+
let messageText = data.text || data.message || data.result || data.response || '';
|
|
1178
|
+
|
|
1179
|
+
// For task_complete/session_done, extract text if present
|
|
1180
|
+
if (!messageText && (event.type === 'task_complete' || event.type === 'session_done')) {
|
|
1181
|
+
messageText = data.final_message || data.summary || '';
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
logger.debug('[Chat] Final message text:', messageText ? messageText.substring(0, 100) : 'EMPTY');
|
|
1185
|
+
|
|
1186
|
+
// Mark session as completed (disable ESC interrupt)
|
|
1187
|
+
this.sessionStatus = 'completed';
|
|
1188
|
+
|
|
1189
|
+
// Create final response chunk if there's text
|
|
1190
|
+
if (messageText) {
|
|
1191
|
+
const chunkId = `chunk_final_${event._id || event._seq || Date.now()}`;
|
|
1192
|
+
|
|
1193
|
+
const chunk = {
|
|
1194
|
+
id: chunkId,
|
|
1195
|
+
type: 'agent_chunk',
|
|
1196
|
+
chunkType: 'response',
|
|
1197
|
+
seq: event._seq || (event._ts_ms || Date.now()),
|
|
1198
|
+
timestamp: event._ts_ms ? event._ts_ms / 1000 : (event.timestamp || Date.now() / 1000),
|
|
1199
|
+
groupId: `group_${this._currentIteration}`,
|
|
1200
|
+
iterationNumber: this._currentIteration,
|
|
1201
|
+
data: { text: messageText }
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
logger.debug('[Chat] š Creating final response chunk:', chunk.id);
|
|
1205
|
+
this.addMessage(chunk);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Reset iteration state for next task
|
|
1209
|
+
this._resetIterationState();
|
|
1210
|
+
|
|
1211
|
+
// Trigger auto-collapse of completed groups
|
|
1212
|
+
this._checkAutoCollapse();
|
|
1213
|
+
|
|
1214
|
+
// Legacy cleanup (for backwards compatibility)
|
|
1215
|
+
if (this.currentAgentMessage) {
|
|
1216
|
+
this._resetAgentMessageState();
|
|
1217
|
+
}
|
|
1218
|
+
this._cleanupOrphanThinkingIndicators();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Handle thinking update events from the agent
|
|
1223
|
+
*
|
|
1224
|
+
* MESSAGE FORMAT v3: Creates hierarchical iterations with file tracking
|
|
1225
|
+
* - Each event creates/updates an iteration object
|
|
1226
|
+
* - Iterations contain: iterationNumber, type, status, reasoning, actions[], files
|
|
1227
|
+
* - Supports different iteration types: thinking, browser, filesystem, mcp, mixed, done
|
|
1228
|
+
*/
|
|
1229
|
+
handleThinkingUpdate(event) {
|
|
1230
|
+
logger.debug('[Chat] handleThinkingUpdate:', event.type, 'sessionStatus=', this.sessionStatus);
|
|
1231
|
+
|
|
1232
|
+
// GUARD: Don't create new messages if session is marked completed
|
|
1233
|
+
if (this.sessionStatus === 'completed') {
|
|
1234
|
+
logger.debug('[Chat] Ignoring thinking update - session completed');
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Mark session as running
|
|
1239
|
+
this.sessionStatus = 'running';
|
|
1240
|
+
|
|
1241
|
+
const data = event.data || {};
|
|
1242
|
+
|
|
1243
|
+
// Update current iteration number for grouping
|
|
1244
|
+
const iterationNumber = data.iteration || data.step || this._currentIteration || 1;
|
|
1245
|
+
if (iterationNumber > this._currentIteration) {
|
|
1246
|
+
this._currentIteration = iterationNumber;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Generate unique chunk ID from event
|
|
1250
|
+
const chunkId = `chunk_${event._id || event._seq || Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
1251
|
+
|
|
1252
|
+
// Check for duplicate events (using _id if available)
|
|
1253
|
+
if (event._id && this._processedEventIds.has(event._id)) {
|
|
1254
|
+
logger.debug('[Chat] āļø Skipping duplicate event:', event._id);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (event._id) {
|
|
1258
|
+
this._processedEventIds.add(event._id);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Create discrete chunk message
|
|
1262
|
+
const chunk = {
|
|
1263
|
+
id: chunkId,
|
|
1264
|
+
type: 'agent_chunk',
|
|
1265
|
+
chunkType: this._determineChunkType(event),
|
|
1266
|
+
seq: event._seq || (event._ts_ms || Date.now()), // Use _seq for ordering
|
|
1267
|
+
timestamp: event._ts_ms ? event._ts_ms / 1000 : (event.timestamp || Date.now() / 1000),
|
|
1268
|
+
groupId: `group_${iterationNumber}`,
|
|
1269
|
+
iterationNumber: iterationNumber,
|
|
1270
|
+
data: data
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
logger.debug('[Chat] š Creating chunk:', chunk.chunkType, 'seq:', chunk.seq, 'group:', chunk.groupId);
|
|
1274
|
+
|
|
1275
|
+
// Add as discrete message - will be inserted at correct chronological position
|
|
1276
|
+
this.addMessage(chunk);
|
|
1277
|
+
|
|
1278
|
+
// Auto-scroll if not locked
|
|
1279
|
+
if (!this.scrollLocked) {
|
|
1280
|
+
this.scrollToBottom(true);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Handle iteration complete event - finalizes an iteration with file info
|
|
1286
|
+
*/
|
|
1287
|
+
handleIterationComplete(event) {
|
|
1288
|
+
const data = event.data || {};
|
|
1289
|
+
const iterationNumber = data.iteration || 1;
|
|
1290
|
+
|
|
1291
|
+
if (!this.currentAgentMessage) {
|
|
1292
|
+
logger.warn('[Chat] iteration_complete but no current message');
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// Ensure iterations array exists (safety for older message formats)
|
|
1297
|
+
if (!this.currentAgentMessage.iterations) {
|
|
1298
|
+
this.currentAgentMessage.iterations = [];
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Find the iteration
|
|
1302
|
+
let iteration = this.currentAgentMessage.iterations.find(
|
|
1303
|
+
i => i.iterationNumber === iterationNumber
|
|
1304
|
+
);
|
|
1305
|
+
|
|
1306
|
+
if (!iteration) {
|
|
1307
|
+
// Create if missing (late event) - actions come from step event, not here
|
|
1308
|
+
iteration = {
|
|
1309
|
+
iterationNumber,
|
|
1310
|
+
timestamp: event.timestamp || (Date.now() / 1000),
|
|
1311
|
+
type: data.iteration_type || 'mixed',
|
|
1312
|
+
status: data.iteration_status || 'completed',
|
|
1313
|
+
reasoning: data.reasoning_summary || '',
|
|
1314
|
+
actions: [], // Actions come from step event
|
|
1315
|
+
filesCreated: data.files_created || [],
|
|
1316
|
+
filesModified: data.files_modified || [],
|
|
1317
|
+
filesRead: data.files_read || [],
|
|
1318
|
+
error: data.error || null,
|
|
1319
|
+
isDone: data.is_done || false
|
|
1320
|
+
};
|
|
1321
|
+
this.currentAgentMessage.iterations.push(iteration);
|
|
1322
|
+
} else {
|
|
1323
|
+
// Update existing iteration with completion status
|
|
1324
|
+
iteration.type = data.iteration_type || iteration.type;
|
|
1325
|
+
iteration.status = data.iteration_status || 'completed';
|
|
1326
|
+
iteration.filesCreated = data.files_created || iteration.filesCreated;
|
|
1327
|
+
iteration.filesModified = data.files_modified || iteration.filesModified;
|
|
1328
|
+
iteration.filesRead = data.files_read || iteration.filesRead;
|
|
1329
|
+
iteration.error = data.error || iteration.error;
|
|
1330
|
+
iteration.isDone = data.is_done || iteration.isDone;
|
|
1331
|
+
// Note: Don't touch actions - they come from step event only
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
logger.debug('[Chat] ā
Iteration', iterationNumber, 'completed with status:', iteration.status);
|
|
1335
|
+
|
|
1336
|
+
this.updateThinkingDisplay();
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
/**
|
|
1340
|
+
* Classify iteration type based on actions
|
|
1341
|
+
*/
|
|
1342
|
+
classifyIterationType(actions) {
|
|
1343
|
+
if (!actions || actions.length === 0) {
|
|
1344
|
+
return 'thinking';
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const services = new Set(actions.map(a => a.service).filter(Boolean));
|
|
1348
|
+
|
|
1349
|
+
// Check for done action
|
|
1350
|
+
if (actions.some(a => a.name === 'done')) {
|
|
1351
|
+
return 'done';
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (services.size === 1) {
|
|
1355
|
+
const service = services.values().next().value;
|
|
1356
|
+
if (service === 'browser') return 'browser';
|
|
1357
|
+
if (service === 'filesystem') return 'filesystem';
|
|
1358
|
+
if (service === 'mcp') return 'mcp';
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
return 'mixed';
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Generate a unique key for an action to prevent duplicates
|
|
1366
|
+
* Uses service:name:normalized_params for reliable deduplication
|
|
1367
|
+
*/
|
|
1368
|
+
getActionKey(action) {
|
|
1369
|
+
const service = action.service || this.detectService(action.name || action.action_type);
|
|
1370
|
+
const name = action.name || action.action_type || 'unknown';
|
|
1371
|
+
|
|
1372
|
+
// Extract key identifying params (sorted for consistency)
|
|
1373
|
+
const params = action.params || {};
|
|
1374
|
+
const keyParams = {};
|
|
1375
|
+
|
|
1376
|
+
// Priority params that uniquely identify actions
|
|
1377
|
+
const priorityKeys = ['url', 'file_path', 'path', 'query', 'text', 'selector', 'element', 'index', 'message'];
|
|
1378
|
+
for (const key of priorityKeys) {
|
|
1379
|
+
if (params[key] !== undefined) {
|
|
1380
|
+
keyParams[key] = params[key];
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// If no priority params, use first few params (sorted by key)
|
|
1385
|
+
if (Object.keys(keyParams).length === 0) {
|
|
1386
|
+
const sortedKeys = Object.keys(params).sort().slice(0, 3);
|
|
1387
|
+
for (const key of sortedKeys) {
|
|
1388
|
+
keyParams[key] = params[key];
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Create a normalized string with sorted keys
|
|
1393
|
+
const sortedParams = Object.keys(keyParams)
|
|
1394
|
+
.sort()
|
|
1395
|
+
.map(k => `${k}=${String(keyParams[k]).substring(0, 100)}`)
|
|
1396
|
+
.join('|');
|
|
1397
|
+
|
|
1398
|
+
return `${service}:${name}:${sortedParams}`;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
parseAction(action) {
|
|
1402
|
+
let service, name, params;
|
|
1403
|
+
|
|
1404
|
+
// Format 1: {service: "x", name: "y", params: {...}} - processed format from AgentStepFormatter
|
|
1405
|
+
if (action.service && (action.name || action.action_type)) {
|
|
1406
|
+
service = action.service;
|
|
1407
|
+
name = action.name || action.action_type;
|
|
1408
|
+
// Use params directly if it exists, otherwise collect other keys
|
|
1409
|
+
if (action.params && typeof action.params === 'object') {
|
|
1410
|
+
params = action.params;
|
|
1411
|
+
} else {
|
|
1412
|
+
params = {};
|
|
1413
|
+
Object.keys(action).forEach(key => {
|
|
1414
|
+
if (key !== 'service' && key !== 'name' && key !== 'action_type' && key !== 'status' && key !== 'params') {
|
|
1415
|
+
params[key] = action[key];
|
|
1416
|
+
}
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
// Format 2: {action_name: {params}} - raw format from model_dump()
|
|
1421
|
+
else if (typeof action === 'object') {
|
|
1422
|
+
const actionKeys = Object.keys(action).filter(k => k !== 'status');
|
|
1423
|
+
if (actionKeys.length > 0) {
|
|
1424
|
+
const actionName = actionKeys[0];
|
|
1425
|
+
name = actionName;
|
|
1426
|
+
service = this.detectService(actionName);
|
|
1427
|
+
params = typeof action[actionName] === 'object' ? action[actionName] : {};
|
|
1428
|
+
} else {
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
// Format 3: string action name
|
|
1433
|
+
else if (typeof action === 'string') {
|
|
1434
|
+
name = action;
|
|
1435
|
+
service = this.detectService(action);
|
|
1436
|
+
params = {};
|
|
1437
|
+
}
|
|
1438
|
+
else {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
return { service, name, params, status: action.status || 'completed' };
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
detectService(actionName) {
|
|
1446
|
+
if (!actionName || typeof actionName !== 'string') {
|
|
1447
|
+
return 'unknown';
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// Normalize action name
|
|
1451
|
+
const normalized = actionName.toLowerCase().trim();
|
|
1452
|
+
|
|
1453
|
+
// FIRST: Check action registry from available_actions event (the source of truth)
|
|
1454
|
+
if (this.actionRegistry[normalized]) {
|
|
1455
|
+
return this.actionRegistry[normalized];
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// FALLBACK: Use heuristics for actions not in registry (backwards compatibility)
|
|
1459
|
+
const serviceMap = {
|
|
1460
|
+
// Browser actions
|
|
1461
|
+
'browser_open_tab': 'browser',
|
|
1462
|
+
'browser_close_tab': 'browser',
|
|
1463
|
+
'browser_click': 'browser',
|
|
1464
|
+
'browser_type': 'browser',
|
|
1465
|
+
'browser_navigate': 'browser',
|
|
1466
|
+
'browser_go_to_url': 'browser',
|
|
1467
|
+
'browser_search_google': 'browser',
|
|
1468
|
+
'browser_screenshot': 'browser',
|
|
1469
|
+
'open_tab': 'browser',
|
|
1470
|
+
'close_tab': 'browser',
|
|
1471
|
+
'click': 'browser',
|
|
1472
|
+
'type_text': 'browser',
|
|
1473
|
+
'navigate': 'browser',
|
|
1474
|
+
'screenshot': 'browser',
|
|
1475
|
+
'go_to_url': 'browser',
|
|
1476
|
+
'search_google': 'browser',
|
|
1477
|
+
|
|
1478
|
+
// Filesystem actions
|
|
1479
|
+
'filesystem_write': 'filesystem',
|
|
1480
|
+
'filesystem_read': 'filesystem',
|
|
1481
|
+
'filesystem_list': 'filesystem',
|
|
1482
|
+
'filesystem_delete': 'filesystem',
|
|
1483
|
+
'filesystem_create_dir': 'filesystem',
|
|
1484
|
+
'write_file': 'filesystem',
|
|
1485
|
+
'read_file': 'filesystem',
|
|
1486
|
+
'list_files': 'filesystem',
|
|
1487
|
+
'delete_file': 'filesystem',
|
|
1488
|
+
'create_directory': 'filesystem',
|
|
1489
|
+
'list_dir': 'filesystem',
|
|
1490
|
+
|
|
1491
|
+
// Task tool actions
|
|
1492
|
+
'create_todo': 'task',
|
|
1493
|
+
'task_create_todo': 'task',
|
|
1494
|
+
'update_todo': 'task',
|
|
1495
|
+
'task_update_todo': 'task',
|
|
1496
|
+
'complete_todo': 'task',
|
|
1497
|
+
'task_complete_todo': 'task',
|
|
1498
|
+
|
|
1499
|
+
// System/command actions
|
|
1500
|
+
'execute_command': 'system',
|
|
1501
|
+
'run_command': 'system',
|
|
1502
|
+
'bash': 'system',
|
|
1503
|
+
'done': 'system',
|
|
1504
|
+
'wait_for_response': 'system'
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
// Direct match
|
|
1508
|
+
if (serviceMap[normalized]) {
|
|
1509
|
+
return serviceMap[normalized];
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Check if action name contains a known service prefix
|
|
1513
|
+
if (normalized.includes('browser')) return 'browser';
|
|
1514
|
+
if (normalized.includes('filesystem') || normalized.includes('file')) return 'filesystem';
|
|
1515
|
+
if (normalized.includes('mcp')) return 'mcp';
|
|
1516
|
+
if (normalized.includes('task') || normalized.includes('todo')) return 'task';
|
|
1517
|
+
|
|
1518
|
+
// Extract service from pattern: "service_action" -> "service"
|
|
1519
|
+
if (normalized.includes('_')) {
|
|
1520
|
+
const parts = normalized.split('_');
|
|
1521
|
+
const possibleService = parts[0];
|
|
1522
|
+
|
|
1523
|
+
// Known services
|
|
1524
|
+
if (['browser', 'filesystem', 'system', 'mcp', 'task'].includes(possibleService)) {
|
|
1525
|
+
return possibleService;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Check last part for service indicators
|
|
1529
|
+
if (parts.includes('file') || parts.includes('dir') || parts.includes('directory')) {
|
|
1530
|
+
return 'filesystem';
|
|
1531
|
+
}
|
|
1532
|
+
if (parts.includes('url') || parts.includes('tab') || parts.includes('click')) {
|
|
1533
|
+
return 'browser';
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// If no match found, use the first part before underscore or return the whole name
|
|
1538
|
+
if (normalized.includes('_')) {
|
|
1539
|
+
return normalized.split('_')[0];
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
return normalized;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* Update the thinking message display
|
|
1547
|
+
*
|
|
1548
|
+
* Re-renders the message with updated steps using hierarchical format
|
|
1549
|
+
* CRITICAL: Never creates new elements - only updates existing ones
|
|
1550
|
+
*/
|
|
1551
|
+
updateThinkingDisplay() {
|
|
1552
|
+
if (!this.currentAgentMessage) {
|
|
1553
|
+
logger.debug('[Chat] updateThinkingDisplay: no currentAgentMessage');
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const msgEl = document.querySelector(`[data-message-id="${this.currentAgentMessage.id}"]`);
|
|
1558
|
+
if (!msgEl) {
|
|
1559
|
+
// FIX: Don't create duplicates - element should already exist from addMessage()
|
|
1560
|
+
// If it doesn't exist, log error but don't create a new one
|
|
1561
|
+
logger.error('[Chat] ā updateThinkingDisplay: DOM element not found for', this.currentAgentMessage.id);
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Re-render using renderAgentMessage for consistent rendering (handles time formatting internally)
|
|
1566
|
+
msgEl.innerHTML = this.renderAgentMessage(this.currentAgentMessage);
|
|
1567
|
+
|
|
1568
|
+
logger.debug('[Chat] š Updated thinking display - steps:',
|
|
1569
|
+
this.currentAgentMessage.steps?.length || 0,
|
|
1570
|
+
'status:', this.currentAgentMessage.status);
|
|
1571
|
+
|
|
1572
|
+
// Auto-scroll if not locked
|
|
1573
|
+
if (!this.scrollLocked) {
|
|
1574
|
+
this.scrollToBottom(true);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
onStreamChunk(data) {
|
|
1579
|
+
logger.debug('[Chat] Stream chunk received:', data);
|
|
1580
|
+
|
|
1581
|
+
// Extract chunk text from data
|
|
1582
|
+
const chunkText = data.chunk || data.text || data.content || '';
|
|
1583
|
+
|
|
1584
|
+
if (!chunkText) {
|
|
1585
|
+
logger.warn('[Chat] Stream chunk has no text content');
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// Create thinking message if needed
|
|
1590
|
+
if (!this.currentAgentMessage) {
|
|
1591
|
+
logger.debug('[Chat] Creating new thinking message for streaming');
|
|
1592
|
+
const messageId = `msg_thinking_${Date.now()}`;
|
|
1593
|
+
this.currentAgentMessage = {
|
|
1594
|
+
id: messageId,
|
|
1595
|
+
type: 'agent',
|
|
1596
|
+
thinking: '',
|
|
1597
|
+
steps: [],
|
|
1598
|
+
status: 'thinking',
|
|
1599
|
+
timestamp: Date.now() / 1000
|
|
1600
|
+
};
|
|
1601
|
+
this.currentAgentMessageId = messageId;
|
|
1602
|
+
this.addMessage(this.currentAgentMessage);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
// Append chunk to thinking text
|
|
1606
|
+
this.currentAgentMessage.thinking += chunkText;
|
|
1607
|
+
this.isStreaming = true;
|
|
1608
|
+
|
|
1609
|
+
logger.debug('[Chat] Thinking message updated, length:', this.currentAgentMessage.thinking.length);
|
|
1610
|
+
|
|
1611
|
+
// Update display - this now updates content directly without full re-render
|
|
1612
|
+
this.updateThinkingDisplay();
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
addMessage(message, options = {}) {
|
|
1616
|
+
// Filter out LLM request messages - they are internal API calls
|
|
1617
|
+
if (message.type === 'llm_request') {
|
|
1618
|
+
return; // Skip LLM requests only - show tool results
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// Remove empty state if present - ALWAYS hide when messages exist
|
|
1622
|
+
if (this.emptyState) {
|
|
1623
|
+
this.emptyState.style.display = 'none';
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Hide config panel if present (only on first message)
|
|
1627
|
+
if (this.messages.length === 0) {
|
|
1628
|
+
const configPanel = document.getElementById('config-panel');
|
|
1629
|
+
if (configPanel) {
|
|
1630
|
+
configPanel.style.display = 'none';
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
// Add to state
|
|
1635
|
+
this.messages.push(message);
|
|
1636
|
+
|
|
1637
|
+
// Trim messages to prevent unbounded memory growth
|
|
1638
|
+
this._trimMessages();
|
|
1639
|
+
|
|
1640
|
+
// Render
|
|
1641
|
+
this.renderMessage(message);
|
|
1642
|
+
|
|
1643
|
+
// Scroll to bottom (unless disabled)
|
|
1644
|
+
if (!options.skipScroll && !this.scrollLocked) {
|
|
1645
|
+
this.scrollToBottom(true);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
updateCurrentAgentMessage(updates) {
|
|
1650
|
+
if (!this.currentAgentMessage) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Merge updates
|
|
1655
|
+
Object.assign(this.currentAgentMessage, updates);
|
|
1656
|
+
|
|
1657
|
+
// Update DOM
|
|
1658
|
+
const msgEl = document.querySelector(`[data-message-id="${this.currentAgentMessage.id}"]`);
|
|
1659
|
+
if (msgEl) {
|
|
1660
|
+
// Re-render the message
|
|
1661
|
+
const newEl = this.createMessageElement(this.currentAgentMessage);
|
|
1662
|
+
msgEl.replaceWith(newEl);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// If completed, clear current (with delay to allow events to process)
|
|
1666
|
+
if (this.currentAgentMessage.status === 'completed') {
|
|
1667
|
+
setTimeout(() => this._resetAgentMessageState(), 500);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
renderMessage(message) {
|
|
1672
|
+
const messageEl = this.createMessageElement(message);
|
|
1673
|
+
|
|
1674
|
+
// Find the correct position to insert based on sequence number (_seq)
|
|
1675
|
+
// Use _seq for ordering (server-guaranteed chronological order)
|
|
1676
|
+
// Fall back to timestamp * 1000 for messages without _seq
|
|
1677
|
+
const existingMessages = this.chatContainer.querySelectorAll('.chat-message');
|
|
1678
|
+
let insertBefore = null;
|
|
1679
|
+
|
|
1680
|
+
const msgSeq = message.seq || (message.timestamp ? message.timestamp * 1000 : 0) || 0;
|
|
1681
|
+
|
|
1682
|
+
for (const existing of existingMessages) {
|
|
1683
|
+
const existingSeq = parseFloat(existing.dataset.seq) || 0;
|
|
1684
|
+
// If we find a message with a later seq, insert before it
|
|
1685
|
+
if (existingSeq > msgSeq) {
|
|
1686
|
+
insertBefore = existing;
|
|
1687
|
+
break;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
if (insertBefore) {
|
|
1692
|
+
// Insert at correct chronological position
|
|
1693
|
+
this.chatContainer.insertBefore(messageEl, insertBefore);
|
|
1694
|
+
} else {
|
|
1695
|
+
// Append at end (newest)
|
|
1696
|
+
this.chatContainer.appendChild(messageEl);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// Update chunk grouping if this is a chunk message
|
|
1700
|
+
if (message.type === 'agent_chunk') {
|
|
1701
|
+
this._updateChunkGroupVisuals();
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
createMessageElement(message) {
|
|
1706
|
+
const wrapper = document.createElement('div');
|
|
1707
|
+
wrapper.className = `chat-message ${message.type}-message`;
|
|
1708
|
+
wrapper.dataset.messageId = message.id;
|
|
1709
|
+
wrapper.dataset.timestamp = message.timestamp || 0; // Legacy support
|
|
1710
|
+
// Use seq for ordering (server-guaranteed chronological order)
|
|
1711
|
+
wrapper.dataset.seq = message.seq || (message.timestamp ? message.timestamp * 1000 : 0) || 0;
|
|
1712
|
+
|
|
1713
|
+
if (message.type === 'user') {
|
|
1714
|
+
wrapper.innerHTML = this.renderUserMessage(message);
|
|
1715
|
+
} else if (message.type === 'agent') {
|
|
1716
|
+
wrapper.innerHTML = this.renderAgentMessage(message);
|
|
1717
|
+
} else if (message.type === 'agent_chunk') {
|
|
1718
|
+
// Discrete timestamped chunk message
|
|
1719
|
+
wrapper.innerHTML = this.renderAgentChunk(message);
|
|
1720
|
+
if (message.groupId) {
|
|
1721
|
+
wrapper.dataset.groupId = message.groupId;
|
|
1722
|
+
}
|
|
1723
|
+
} else if (message.type === 'system') {
|
|
1724
|
+
wrapper.innerHTML = this.renderSystemMessage(message);
|
|
1725
|
+
} else if (message.type === 'llm_request') {
|
|
1726
|
+
wrapper.innerHTML = this.renderLLMRequest(message);
|
|
1727
|
+
} else if (message.type === 'tool_execution') {
|
|
1728
|
+
wrapper.innerHTML = this.renderToolExecution(message);
|
|
1729
|
+
} else if (message.type === 'error') {
|
|
1730
|
+
wrapper.innerHTML = this.renderErrorMessage(message);
|
|
1731
|
+
} else if (message.type === 'multi_agent_relationship') {
|
|
1732
|
+
wrapper.innerHTML = this.renderMultiAgentRelationship(message);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// Apply Prism syntax highlighting to code blocks (if Prism is loaded)
|
|
1736
|
+
if (window.Prism) {
|
|
1737
|
+
requestAnimationFrame(() => {
|
|
1738
|
+
Prism.highlightAllUnder(wrapper);
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
return wrapper;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
/**
|
|
1746
|
+
* Render a discrete agent chunk message
|
|
1747
|
+
* Chunks are individual timestamped events that display in chronological order
|
|
1748
|
+
*/
|
|
1749
|
+
renderAgentChunk(message) {
|
|
1750
|
+
const time = this._formatTime(message.timestamp);
|
|
1751
|
+
const data = message.data || {};
|
|
1752
|
+
|
|
1753
|
+
switch (message.chunkType) {
|
|
1754
|
+
case 'action':
|
|
1755
|
+
return this._renderActionChunk(time, data, message);
|
|
1756
|
+
case 'thinking':
|
|
1757
|
+
return this._renderThinkingChunk(time, data, message);
|
|
1758
|
+
case 'response':
|
|
1759
|
+
return this._renderResponseChunk(time, data, message);
|
|
1760
|
+
case 'error':
|
|
1761
|
+
return this._renderErrorChunk(time, data, message);
|
|
1762
|
+
case 'file':
|
|
1763
|
+
return this._renderFileChunk(time, data, message);
|
|
1764
|
+
case 'status':
|
|
1765
|
+
return this._renderStatusChunk(time, data, message);
|
|
1766
|
+
default:
|
|
1767
|
+
// Generic chunk display
|
|
1768
|
+
return this._renderGenericChunk(time, data, message);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
_renderActionChunk(time, data, message) {
|
|
1773
|
+
const actions = data.actions || [];
|
|
1774
|
+
if (actions.length === 0) {
|
|
1775
|
+
return this._renderGenericChunk(time, data, message);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
let html = `${this._renderMessageHeader(time, 'agent')}`;
|
|
1779
|
+
html += '<div class="chunk-actions">';
|
|
1780
|
+
|
|
1781
|
+
actions.forEach(action => {
|
|
1782
|
+
const parsedAction = this.parseAction(action);
|
|
1783
|
+
if (parsedAction) {
|
|
1784
|
+
html += this.renderStepAction(parsedAction);
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
html += '</div>';
|
|
1789
|
+
return html;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
_renderThinkingChunk(time, data, message) {
|
|
1793
|
+
const reasoning = data.reasoning || data.thought || data.task_progress ||
|
|
1794
|
+
data.next_goal || data.current_task || data.plan || '';
|
|
1795
|
+
|
|
1796
|
+
if (!reasoning) {
|
|
1797
|
+
// Empty thinking - show minimal indicator
|
|
1798
|
+
return `${this._renderMessageHeader(time, 'agent')} ${this._renderThinkingIndicator()}`;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
return `${this._renderMessageHeader(time, 'agent')}<div class="chunk-thinking"><span class="step-thinking-line">${escapeHtml(reasoning)}</span></div>`;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
_renderResponseChunk(time, data, message) {
|
|
1805
|
+
const text = data.text || data.message || data.response || '';
|
|
1806
|
+
if (!text) {
|
|
1807
|
+
return '';
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
return `${this._renderMessageHeader(time, 'agent')}<div class="chunk-response final-response"><span class="message-content">${escapeHtml(text)}</span></div>`;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
_renderErrorChunk(time, data, message) {
|
|
1814
|
+
const errorMsg = data.error_message || data.error || data.message || 'Unknown error';
|
|
1815
|
+
return `${this._renderMessageHeader(time, 'agent')}<div class="chunk-error iteration-error">${escapeHtml(errorMsg)}</div>`;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
_renderFileChunk(time, data, message) {
|
|
1819
|
+
let html = `${this._renderMessageHeader(time, 'agent')}<div class="chunk-files iteration-files">`;
|
|
1820
|
+
|
|
1821
|
+
// Created files
|
|
1822
|
+
if (data.files_created?.length > 0) {
|
|
1823
|
+
data.files_created.forEach(filePath => {
|
|
1824
|
+
const fileName = filePath.split('/').pop();
|
|
1825
|
+
html += `<span class="file-chip file-created" title="${escapeHtml(filePath)}"><span class="file-chip-icon">+</span><span class="file-chip-name">${escapeHtml(fileName)}</span></span>`;
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// Modified files
|
|
1830
|
+
if (data.files_modified?.length > 0) {
|
|
1831
|
+
data.files_modified.forEach(filePath => {
|
|
1832
|
+
const fileName = filePath.split('/').pop();
|
|
1833
|
+
html += `<span class="file-chip file-modified" title="${escapeHtml(filePath)}"><span class="file-chip-icon">~</span><span class="file-chip-name">${escapeHtml(fileName)}</span></span>`;
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
html += '</div>';
|
|
1838
|
+
return html;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
_renderStatusChunk(time, data, message) {
|
|
1842
|
+
const status = data.status || 'unknown';
|
|
1843
|
+
const statusText = status.charAt(0).toUpperCase() + status.slice(1);
|
|
1844
|
+
return `${this._renderMessageHeader(time, 'agent')}<div class="chunk-status"><span class="status-indicator status-${status}">${escapeHtml(statusText)}</span></div>`;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
_renderGenericChunk(time, data, message) {
|
|
1848
|
+
// For unrecognized chunk types, show a minimal representation
|
|
1849
|
+
const summary = data.reasoning || data.task_progress || data.status || '';
|
|
1850
|
+
if (summary) {
|
|
1851
|
+
return `${this._renderMessageHeader(time, 'agent')}<span class="chunk-generic">${escapeHtml(summary)}</span>`;
|
|
1852
|
+
}
|
|
1853
|
+
return `${this._renderMessageHeader(time, 'agent')}<span class="chunk-generic text-muted">(processing...)</span>`;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* Determine chunk type from event data
|
|
1858
|
+
*/
|
|
1859
|
+
_determineChunkType(event) {
|
|
1860
|
+
const data = event.data || {};
|
|
1861
|
+
|
|
1862
|
+
// Check for actions first
|
|
1863
|
+
if (data.actions && Array.isArray(data.actions) && data.actions.length > 0) {
|
|
1864
|
+
return 'action';
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
// Check for file operations
|
|
1868
|
+
if ((data.files_created?.length > 0) || (data.files_modified?.length > 0)) {
|
|
1869
|
+
return 'file';
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// Check for errors
|
|
1873
|
+
if (data.error_message || data.error) {
|
|
1874
|
+
return 'error';
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Check for final response
|
|
1878
|
+
if (data.is_done || data.final_message || data.result) {
|
|
1879
|
+
return 'response';
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// Check for status change
|
|
1883
|
+
if (data.status && ['completed', 'done', 'paused', 'cancelled'].includes(data.status)) {
|
|
1884
|
+
return 'status';
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Default to thinking
|
|
1888
|
+
return 'thinking';
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* Update visual grouping for chunk messages
|
|
1893
|
+
*/
|
|
1894
|
+
_updateChunkGroupVisuals() {
|
|
1895
|
+
const chunks = this.chatContainer.querySelectorAll('.agent_chunk-message');
|
|
1896
|
+
if (chunks.length === 0) return;
|
|
1897
|
+
|
|
1898
|
+
let currentGroup = null;
|
|
1899
|
+
let groupStart = null;
|
|
1900
|
+
|
|
1901
|
+
chunks.forEach((chunk, index) => {
|
|
1902
|
+
const groupId = chunk.dataset.groupId;
|
|
1903
|
+
|
|
1904
|
+
// Remove old classes
|
|
1905
|
+
chunk.classList.remove('chunk-group-start', 'chunk-group-end', 'chunk-group-middle');
|
|
1906
|
+
|
|
1907
|
+
if (groupId !== currentGroup) {
|
|
1908
|
+
// New group starting
|
|
1909
|
+
if (groupStart) {
|
|
1910
|
+
// Mark end of previous group
|
|
1911
|
+
groupStart.classList.add('chunk-group-start');
|
|
1912
|
+
chunks[index - 1]?.classList.add('chunk-group-end');
|
|
1913
|
+
}
|
|
1914
|
+
currentGroup = groupId;
|
|
1915
|
+
groupStart = chunk;
|
|
1916
|
+
} else {
|
|
1917
|
+
// Continuing same group
|
|
1918
|
+
chunk.classList.add('chunk-group-middle');
|
|
1919
|
+
}
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
// Mark the last group
|
|
1923
|
+
if (groupStart) {
|
|
1924
|
+
groupStart.classList.add('chunk-group-start');
|
|
1925
|
+
chunks[chunks.length - 1]?.classList.add('chunk-group-end');
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// Check for auto-collapse after visual update
|
|
1929
|
+
this._checkAutoCollapse();
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* Auto-collapse completed groups with more than 5 chunks
|
|
1934
|
+
*/
|
|
1935
|
+
_checkAutoCollapse() {
|
|
1936
|
+
// Group chunks by groupId
|
|
1937
|
+
const groups = new Map();
|
|
1938
|
+
this.messages.forEach(msg => {
|
|
1939
|
+
if (msg.type === 'agent_chunk' && msg.groupId) {
|
|
1940
|
+
if (!groups.has(msg.groupId)) {
|
|
1941
|
+
groups.set(msg.groupId, []);
|
|
1942
|
+
}
|
|
1943
|
+
groups.get(msg.groupId).push(msg);
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
// Check each group for auto-collapse
|
|
1948
|
+
const currentGroupId = `group_${this._currentIteration}`;
|
|
1949
|
+
groups.forEach((chunks, groupId) => {
|
|
1950
|
+
// Don't collapse the current active group
|
|
1951
|
+
if (groupId === currentGroupId) return;
|
|
1952
|
+
|
|
1953
|
+
// Don't collapse if already collapsed
|
|
1954
|
+
const groupInfo = this._chunkGroups.get(groupId);
|
|
1955
|
+
if (groupInfo?.collapsed) return;
|
|
1956
|
+
|
|
1957
|
+
// Auto-collapse if more than 5 chunks
|
|
1958
|
+
if (chunks.length > 5) {
|
|
1959
|
+
this._collapseGroup(groupId, chunks);
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/**
|
|
1965
|
+
* Collapse a group of chunks into a summary
|
|
1966
|
+
*/
|
|
1967
|
+
_collapseGroup(groupId, chunks) {
|
|
1968
|
+
if (!chunks || chunks.length === 0) return;
|
|
1969
|
+
|
|
1970
|
+
// Find the DOM elements for this group
|
|
1971
|
+
const groupElements = this.chatContainer.querySelectorAll(`[data-group-id="${groupId}"]`);
|
|
1972
|
+
if (groupElements.length === 0) return;
|
|
1973
|
+
|
|
1974
|
+
// Create summary element
|
|
1975
|
+
const firstChunk = chunks[0];
|
|
1976
|
+
const lastChunk = chunks[chunks.length - 1];
|
|
1977
|
+
const actionCount = chunks.filter(c => c.chunkType === 'action').length;
|
|
1978
|
+
const thinkingCount = chunks.filter(c => c.chunkType === 'thinking').length;
|
|
1979
|
+
|
|
1980
|
+
const summaryHtml = `
|
|
1981
|
+
<div class="chunk-summary" data-group-id="${groupId}">
|
|
1982
|
+
<span class="chunk-summary-icon">ā¶</span>
|
|
1983
|
+
<span class="chunk-summary-text">
|
|
1984
|
+
${chunks.length} steps: ${actionCount} actions, ${thinkingCount} thoughts
|
|
1985
|
+
</span>
|
|
1986
|
+
<span class="chunk-summary-time">${this._formatTime(firstChunk.timestamp)} - ${this._formatTime(lastChunk.timestamp)}</span>
|
|
1987
|
+
<span class="chunk-summary-expand">(click to expand)</span>
|
|
1988
|
+
</div>
|
|
1989
|
+
`;
|
|
1990
|
+
|
|
1991
|
+
// Create wrapper for collapsed content
|
|
1992
|
+
const wrapper = document.createElement('div');
|
|
1993
|
+
wrapper.className = 'chat-message chunk-group-collapsed';
|
|
1994
|
+
wrapper.dataset.groupId = groupId;
|
|
1995
|
+
wrapper.dataset.seq = firstChunk.seq || 0;
|
|
1996
|
+
wrapper.innerHTML = summaryHtml;
|
|
1997
|
+
|
|
1998
|
+
// Add click handler to expand
|
|
1999
|
+
wrapper.addEventListener('click', () => this._expandGroup(groupId));
|
|
2000
|
+
|
|
2001
|
+
// Hide individual chunks and insert summary
|
|
2002
|
+
const firstElement = groupElements[0];
|
|
2003
|
+
firstElement.parentNode.insertBefore(wrapper, firstElement);
|
|
2004
|
+
|
|
2005
|
+
groupElements.forEach(el => {
|
|
2006
|
+
el.style.display = 'none';
|
|
2007
|
+
el.classList.add('chunk-collapsed-item');
|
|
2008
|
+
});
|
|
2009
|
+
|
|
2010
|
+
// Track collapsed state
|
|
2011
|
+
this._chunkGroups.set(groupId, { chunks, collapsed: true });
|
|
2012
|
+
|
|
2013
|
+
logger.debug(`[Chat] Collapsed group ${groupId} with ${chunks.length} chunks`);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Expand a collapsed group
|
|
2018
|
+
*/
|
|
2019
|
+
_expandGroup(groupId) {
|
|
2020
|
+
// Find summary element
|
|
2021
|
+
const summary = this.chatContainer.querySelector(`.chunk-group-collapsed[data-group-id="${groupId}"]`);
|
|
2022
|
+
if (!summary) return;
|
|
2023
|
+
|
|
2024
|
+
// Show individual chunks
|
|
2025
|
+
const groupElements = this.chatContainer.querySelectorAll(`[data-group-id="${groupId}"].chunk-collapsed-item`);
|
|
2026
|
+
groupElements.forEach(el => {
|
|
2027
|
+
el.style.display = '';
|
|
2028
|
+
el.classList.remove('chunk-collapsed-item');
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
// Remove summary
|
|
2032
|
+
summary.remove();
|
|
2033
|
+
|
|
2034
|
+
// Update tracking
|
|
2035
|
+
const groupInfo = this._chunkGroups.get(groupId);
|
|
2036
|
+
if (groupInfo) {
|
|
2037
|
+
groupInfo.collapsed = false;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Re-apply visual grouping
|
|
2041
|
+
this._updateChunkGroupVisuals();
|
|
2042
|
+
|
|
2043
|
+
logger.debug(`[Chat] Expanded group ${groupId}`);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/**
|
|
2047
|
+
* Reset iteration state for new task
|
|
2048
|
+
*/
|
|
2049
|
+
_resetIterationState() {
|
|
2050
|
+
this._currentIteration = 0;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
renderUserMessage(message) {
|
|
2054
|
+
const time = this._formatTime(message.timestamp);
|
|
2055
|
+
// IRC style: [time] <user> message
|
|
2056
|
+
return `${this._renderMessageHeader(time, 'user')}<span class="message-content">${escapeHtml(message.text)}</span>`;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
renderAgentMessage(message) {
|
|
2060
|
+
const time = this._formatTime(message.timestamp);
|
|
2061
|
+
|
|
2062
|
+
// PRIORITY 0: Use iterations if available (v3 format)
|
|
2063
|
+
if (message.iterations && message.iterations.length > 0) {
|
|
2064
|
+
let html = this.renderIterations(message, time);
|
|
2065
|
+
|
|
2066
|
+
// Add final response below if completed
|
|
2067
|
+
if (message.status === 'completed' && message.text) {
|
|
2068
|
+
html += `\n<div class="final-response">${this._renderMessageHeader(time)}<span class="message-content">${escapeHtml(message.text)}</span></div>`;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
return html;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// PRIORITY 1: If completed message with BOTH steps and final text (v2 format)
|
|
2075
|
+
// Show thinking process FIRST, then final response
|
|
2076
|
+
if (message.status === 'completed' && message.text && message.steps && message.steps.length > 0) {
|
|
2077
|
+
// Render steps (thinking + actions)
|
|
2078
|
+
let html = this.renderStepsHierarchical(message, time);
|
|
2079
|
+
|
|
2080
|
+
// Add final response below the thinking process
|
|
2081
|
+
html += `\n<div class="final-response">${this._renderMessageHeader(time)}<span class="message-content">${escapeHtml(message.text)}</span></div>`;
|
|
2082
|
+
|
|
2083
|
+
// Add file preview if detected
|
|
2084
|
+
const fileOp = this.detectFileOperation(message.text);
|
|
2085
|
+
if (fileOp) {
|
|
2086
|
+
html += this.renderFilePreview(fileOp);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
return html;
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
// PRIORITY 2: If completed with text only (no steps)
|
|
2093
|
+
if (message.status === 'completed' && message.text) {
|
|
2094
|
+
// Detect file operations in the message
|
|
2095
|
+
const fileOp = this.detectFileOperation(message.text);
|
|
2096
|
+
|
|
2097
|
+
// Build base message HTML
|
|
2098
|
+
let html = `${this._renderMessageHeader(time)}<span class="message-content">${escapeHtml(message.text)}</span>`;
|
|
2099
|
+
|
|
2100
|
+
// Add file preview if detected
|
|
2101
|
+
if (fileOp) {
|
|
2102
|
+
html += this.renderFilePreview(fileOp);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
return html;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// PRIORITY 3: If this message has STEPS (hierarchical structure - v2 format)
|
|
2109
|
+
if (message.steps && message.steps.length > 0) {
|
|
2110
|
+
return this.renderStepsHierarchical(message, time);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// FALLBACK: Simple thinking indicator or empty message
|
|
2114
|
+
if (message.status === 'thinking') {
|
|
2115
|
+
return `${this._renderMessageHeader(time)} ${this._renderThinkingIndicator(true)}`;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
// Final fallback: render text if available
|
|
2119
|
+
const messageText = message.text || message.result || message.content;
|
|
2120
|
+
|
|
2121
|
+
if (!messageText) {
|
|
2122
|
+
logger.warn('[Chat] Agent message has no text content:', message);
|
|
2123
|
+
return `${this._renderMessageHeader(time)}<span class="message-content text-muted">(no response)</span>`;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
return `${this._renderMessageHeader(time)}<span class="message-content">${escapeHtml(messageText)}</span>`;
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
/**
|
|
2130
|
+
* Render iterations with status indicators and file chips (v3 format)
|
|
2131
|
+
*/
|
|
2132
|
+
renderIterations(message, time) {
|
|
2133
|
+
let html = this._renderMessageHeader(time);
|
|
2134
|
+
|
|
2135
|
+
// Show brain loader if still loading
|
|
2136
|
+
if (message.showBrainLoader) {
|
|
2137
|
+
html += ` ${this._renderThinkingIndicator(true)}`;
|
|
2138
|
+
return html;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
html += '<div class="message-iterations">';
|
|
2142
|
+
|
|
2143
|
+
message.iterations.forEach((iteration) => {
|
|
2144
|
+
html += `<div class="message-iteration">`;
|
|
2145
|
+
|
|
2146
|
+
// Reasoning line (if present) - bullet point style like steps
|
|
2147
|
+
if (iteration.reasoning) {
|
|
2148
|
+
html += `<div class="iteration-reasoning">${escapeHtml(iteration.reasoning)}</div>`;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
// Action lines (if present) - uses existing step-action-line rendering
|
|
2152
|
+
if (iteration.actions && iteration.actions.length > 0) {
|
|
2153
|
+
iteration.actions.forEach(action => {
|
|
2154
|
+
html += this.renderStepAction(action);
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// Error message (if present)
|
|
2159
|
+
if (iteration.error) {
|
|
2160
|
+
html += `<div class="iteration-error">${escapeHtml(iteration.error)}</div>`;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// File chips - ONLY if there are files created/modified
|
|
2164
|
+
const hasFiles = (iteration.filesCreated?.length > 0) ||
|
|
2165
|
+
(iteration.filesModified?.length > 0);
|
|
2166
|
+
|
|
2167
|
+
if (hasFiles) {
|
|
2168
|
+
html += this.renderIterationFileChips(iteration);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
html += '</div>';
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
// Thinking indicator at bottom if still active
|
|
2175
|
+
if (message.status === 'thinking') {
|
|
2176
|
+
html += `<div class="iteration-thinking">${this._renderThinkingIndicator()}</div>`;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
html += '</div>';
|
|
2180
|
+
|
|
2181
|
+
return html;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
/**
|
|
2185
|
+
* Render file chips for an iteration - ONLY called when files exist
|
|
2186
|
+
*/
|
|
2187
|
+
renderIterationFileChips(iteration) {
|
|
2188
|
+
let html = '<div class="iteration-files">';
|
|
2189
|
+
|
|
2190
|
+
// Created files (green)
|
|
2191
|
+
if (iteration.filesCreated?.length > 0) {
|
|
2192
|
+
iteration.filesCreated.forEach(filePath => {
|
|
2193
|
+
const fileName = filePath.split('/').pop();
|
|
2194
|
+
html += `
|
|
2195
|
+
<span class="file-chip file-created" data-path="${escapeHtml(filePath)}" title="${escapeHtml(filePath)}">
|
|
2196
|
+
<span class="file-chip-icon">+</span>
|
|
2197
|
+
<span class="file-chip-name">${escapeHtml(fileName)}</span>
|
|
2198
|
+
</span>
|
|
2199
|
+
`;
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// Modified files (yellow)
|
|
2204
|
+
if (iteration.filesModified?.length > 0) {
|
|
2205
|
+
iteration.filesModified.forEach(filePath => {
|
|
2206
|
+
const fileName = filePath.split('/').pop();
|
|
2207
|
+
html += `
|
|
2208
|
+
<span class="file-chip file-modified" data-path="${escapeHtml(filePath)}" title="${escapeHtml(filePath)}">
|
|
2209
|
+
<span class="file-chip-icon">~</span>
|
|
2210
|
+
<span class="file-chip-name">${escapeHtml(fileName)}</span>
|
|
2211
|
+
</span>
|
|
2212
|
+
`;
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// Read files (blue) - optional display
|
|
2217
|
+
if (iteration.filesRead?.length > 0) {
|
|
2218
|
+
iteration.filesRead.forEach(filePath => {
|
|
2219
|
+
const fileName = filePath.split('/').pop();
|
|
2220
|
+
html += `
|
|
2221
|
+
<span class="file-chip file-read" data-path="${escapeHtml(filePath)}" title="${escapeHtml(filePath)}">
|
|
2222
|
+
<span class="file-chip-icon">ā</span>
|
|
2223
|
+
<span class="file-chip-name">${escapeHtml(fileName)}</span>
|
|
2224
|
+
</span>
|
|
2225
|
+
`;
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
html += '</div>';
|
|
2230
|
+
return html;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* Render IRC-style steps display - clean and minimal (v2 format - backwards compat)
|
|
2235
|
+
*/
|
|
2236
|
+
renderStepsHierarchical(message, time) {
|
|
2237
|
+
let html = this._renderMessageHeader(time);
|
|
2238
|
+
|
|
2239
|
+
// Show brain loader if still loading
|
|
2240
|
+
if (message.showBrainLoader) {
|
|
2241
|
+
html += ` ${this._renderThinkingIndicator(true)}`;
|
|
2242
|
+
return html;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// Render steps in IRC style
|
|
2246
|
+
html += '<div class="message-steps">';
|
|
2247
|
+
|
|
2248
|
+
message.steps.forEach((step) => {
|
|
2249
|
+
html += '<div class="message-step">';
|
|
2250
|
+
|
|
2251
|
+
// Thinking line (if present) - bullet added via CSS ::before
|
|
2252
|
+
if (step.thinking) {
|
|
2253
|
+
html += `<div class="step-thinking-line">${escapeHtml(step.thinking)}</div>`;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Action lines (if present)
|
|
2257
|
+
if (step.actions && step.actions.length > 0) {
|
|
2258
|
+
step.actions.forEach(action => {
|
|
2259
|
+
html += this.renderStepAction(action);
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
html += '</div>';
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
// Add simple thinking dots at bottom if still thinking
|
|
2267
|
+
if (message.status === 'thinking') {
|
|
2268
|
+
html += `<div class="step-thinking-dots">${this._renderThinkingIndicator()}</div>`;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
html += '</div>';
|
|
2272
|
+
|
|
2273
|
+
return html;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
/**
|
|
2277
|
+
* Format a parameter value for display (handles objects, arrays, etc.)
|
|
2278
|
+
*/
|
|
2279
|
+
formatParamValue(value) {
|
|
2280
|
+
if (value === null) return 'null';
|
|
2281
|
+
if (value === undefined) return 'undefined';
|
|
2282
|
+
|
|
2283
|
+
// If it's an object or array, stringify it
|
|
2284
|
+
if (typeof value === 'object') {
|
|
2285
|
+
try {
|
|
2286
|
+
return JSON.stringify(value);
|
|
2287
|
+
} catch (e) {
|
|
2288
|
+
return String(value);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
return String(value);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
/**
|
|
2296
|
+
* Render single action line - IRC style with arrow and expandable params
|
|
2297
|
+
*/
|
|
2298
|
+
renderStepAction(action) {
|
|
2299
|
+
const actionId = `action_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
2300
|
+
const service = action.service || '';
|
|
2301
|
+
const name = action.name || 'unknown';
|
|
2302
|
+
|
|
2303
|
+
// Build truncated param preview (show 1-2 key params)
|
|
2304
|
+
let paramPreview = '';
|
|
2305
|
+
let hasParams = false;
|
|
2306
|
+
const paramEntries = [];
|
|
2307
|
+
|
|
2308
|
+
if (action.params && Object.keys(action.params).length > 0) {
|
|
2309
|
+
hasParams = true;
|
|
2310
|
+
|
|
2311
|
+
// Priority order for which params to show in preview
|
|
2312
|
+
const priorityKeys = ['url', 'file_path', 'path', 'query', 'text', 'content', 'selector', 'command'];
|
|
2313
|
+
|
|
2314
|
+
// Find first 2 relevant params for preview
|
|
2315
|
+
for (const key of priorityKeys) {
|
|
2316
|
+
if (action.params[key] !== undefined) {
|
|
2317
|
+
let value = this.formatParamValue(action.params[key]);
|
|
2318
|
+
// Truncate long values
|
|
2319
|
+
if (value.length > 50) {
|
|
2320
|
+
value = value.substring(0, 50) + '...';
|
|
2321
|
+
}
|
|
2322
|
+
paramEntries.push(`${key}: ${value}`);
|
|
2323
|
+
if (paramEntries.length >= 2) break;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// If we didn't find priority params, show first 2 params
|
|
2328
|
+
if (paramEntries.length === 0) {
|
|
2329
|
+
const entries = Object.entries(action.params).slice(0, 2);
|
|
2330
|
+
entries.forEach(([key, value]) => {
|
|
2331
|
+
let val = this.formatParamValue(value);
|
|
2332
|
+
if (val.length > 40) {
|
|
2333
|
+
val = val.substring(0, 40) + '...';
|
|
2334
|
+
}
|
|
2335
|
+
paramEntries.push(`${key}: ${val}`);
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
if (paramEntries.length > 0) {
|
|
2340
|
+
paramPreview = ` <span class="action-params">(${escapeHtml(paramEntries.join(', '))})</span>`;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
// Build the action line with service name if available
|
|
2345
|
+
let html = `<div class="step-action-line" data-action-id="${actionId}">`;
|
|
2346
|
+
if (service) {
|
|
2347
|
+
html += `<span class="action-service">${escapeHtml(service)}</span>`;
|
|
2348
|
+
html += `<span class="action-separator">ā</span>`;
|
|
2349
|
+
}
|
|
2350
|
+
html += `<span class="action-name">${escapeHtml(name)}</span>`;
|
|
2351
|
+
html += paramPreview;
|
|
2352
|
+
html += `</div>`;
|
|
2353
|
+
|
|
2354
|
+
// Build expandable details section
|
|
2355
|
+
if (hasParams) {
|
|
2356
|
+
html += `<div class="action-details" id="${actionId}">`;
|
|
2357
|
+
html += `<div class="action-details-header">Parameters:</div>`;
|
|
2358
|
+
|
|
2359
|
+
// Show ALL params in detail view
|
|
2360
|
+
Object.entries(action.params).forEach(([key, value]) => {
|
|
2361
|
+
let displayValue = this.formatParamValue(value);
|
|
2362
|
+
// Format long values nicely
|
|
2363
|
+
if (displayValue.length > 200) {
|
|
2364
|
+
displayValue = displayValue.substring(0, 200) + '... (truncated)';
|
|
2365
|
+
}
|
|
2366
|
+
html += `<div class="action-param-line">`;
|
|
2367
|
+
html += `<span class="action-param-key">${escapeHtml(key)}:</span> `;
|
|
2368
|
+
html += `<span class="action-param-value">${escapeHtml(displayValue)}</span>`;
|
|
2369
|
+
html += `</div>`;
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
html += `</div>`;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
return html;
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
renderSystemMessage(message) {
|
|
2380
|
+
const time = this._formatTime(message.timestamp);
|
|
2381
|
+
// IRC style system message: [time] * message
|
|
2382
|
+
// Escape HTML first, then linkify URLs
|
|
2383
|
+
let text = escapeHtml(message.text);
|
|
2384
|
+
// Convert t.me/xxx and https:// URLs to clickable links
|
|
2385
|
+
text = text.replace(/(https?:\/\/[^\s<]+|t\.me\/[^\s<]+)/g, '<a href="https://$1" target="_blank" rel="noopener noreferrer">$1</a>');
|
|
2386
|
+
// Fix double https:// for URLs that already had it
|
|
2387
|
+
text = text.replace(/href="https:\/\/https:\/\//g, 'href="https://');
|
|
2388
|
+
return `<span class="message-time">[${time}]</span> <span class="message-author">*</span> <span class="system-message-content">${text}</span>`;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
renderLLMRequest(message) {
|
|
2392
|
+
// Hide LLM requests - they clutter the chat flow
|
|
2393
|
+
// Users care about results, not internal API calls
|
|
2394
|
+
return '';
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
renderToolExecution(message) {
|
|
2398
|
+
// FIX (Dec 31, 2025): Actually display tool execution results for user visibility
|
|
2399
|
+
const time = this._formatTime(message.timestamp);
|
|
2400
|
+
const data = message.data || {};
|
|
2401
|
+
const toolName = data.tool_name || 'unknown';
|
|
2402
|
+
const actionName = data.action_name || 'unknown';
|
|
2403
|
+
const success = data.success !== false;
|
|
2404
|
+
const duration = (data.duration_seconds || 0).toFixed(2);
|
|
2405
|
+
const preview = data.result_preview || '';
|
|
2406
|
+
const error = data.error || '';
|
|
2407
|
+
const resultSize = data.result_size || 0;
|
|
2408
|
+
|
|
2409
|
+
const statusIcon = success ? 'ā
' : 'ā';
|
|
2410
|
+
const statusClass = success ? 'tool-success' : 'tool-error';
|
|
2411
|
+
|
|
2412
|
+
// Build result content
|
|
2413
|
+
let resultHtml = '';
|
|
2414
|
+
if (error) {
|
|
2415
|
+
resultHtml = `<div class="tool-error-msg">${escapeHtml(error)}</div>`;
|
|
2416
|
+
} else if (preview) {
|
|
2417
|
+
// Show preview with size indicator if truncated
|
|
2418
|
+
const truncatedNote = resultSize > preview.length ? ` <span class="tool-truncated">(showing ${preview.length} of ${resultSize.toLocaleString()} chars)</span>` : '';
|
|
2419
|
+
resultHtml = `<div class="tool-result-preview">${escapeHtml(preview)}${truncatedNote}</div>`;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
return `
|
|
2423
|
+
<div class="tool-execution-inline ${statusClass}">
|
|
2424
|
+
<span class="message-time">[${time}]</span>
|
|
2425
|
+
<span class="tool-indicator">${statusIcon}</span>
|
|
2426
|
+
<span class="tool-badge">${escapeHtml(toolName)}</span>
|
|
2427
|
+
<span class="tool-separator">ā</span>
|
|
2428
|
+
<span class="tool-action">${escapeHtml(actionName)}</span>
|
|
2429
|
+
<span class="tool-duration">(${duration}s)</span>
|
|
2430
|
+
${resultHtml}
|
|
2431
|
+
</div>
|
|
2432
|
+
`;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
renderErrorMessage(message) {
|
|
2436
|
+
const time = this._formatTime(message.timestamp);
|
|
2437
|
+
const severityIcon = message.recoverable ? 'ā ļø' : 'ā';
|
|
2438
|
+
const severityClass = message.recoverable ? 'warning' : 'critical';
|
|
2439
|
+
|
|
2440
|
+
let html = `
|
|
2441
|
+
<span class="message-time">[${time}]</span>
|
|
2442
|
+
<span class="message-author error-message">${severityIcon}</span>
|
|
2443
|
+
<span class="error-content ${severityClass}">
|
|
2444
|
+
<span class="error-type">${escapeHtml(message.errorType)}:</span>
|
|
2445
|
+
<span class="error-text">${escapeHtml(message.errorMessage)}</span>
|
|
2446
|
+
</span>
|
|
2447
|
+
`;
|
|
2448
|
+
|
|
2449
|
+
// Add stack trace if available (collapsed by default)
|
|
2450
|
+
if (message.errorStack) {
|
|
2451
|
+
html += `
|
|
2452
|
+
<div class="error-stack-container">
|
|
2453
|
+
<button class="error-stack-toggle" onclick="this.nextElementSibling.classList.toggle('expanded')">
|
|
2454
|
+
Show stack trace
|
|
2455
|
+
</button>
|
|
2456
|
+
<pre class="error-stack">${escapeHtml(message.errorStack)}</pre>
|
|
2457
|
+
</div>
|
|
2458
|
+
`;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
return html;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
/**
|
|
2465
|
+
* Detect if message contains file operation and extract details
|
|
2466
|
+
*/
|
|
2467
|
+
detectFileOperation(messageText) {
|
|
2468
|
+
if (!messageText) return null;
|
|
2469
|
+
|
|
2470
|
+
// Pattern 1: "Created 'filename' with the content 'inline content'" (inline format)
|
|
2471
|
+
const inlineMatch = messageText.match(/(?:Successfully created|Created file|Wrote|Created)\s+([^\s]+)\s+with\s+the\s+content\s+['"]([^'"]+)['"]/i);
|
|
2472
|
+
if (inlineMatch) {
|
|
2473
|
+
const filepath = inlineMatch[1];
|
|
2474
|
+
const content = inlineMatch[2];
|
|
2475
|
+
return {
|
|
2476
|
+
operation: 'create',
|
|
2477
|
+
path: filepath,
|
|
2478
|
+
content: content,
|
|
2479
|
+
language: this.detectLanguage(filepath)
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
// Pattern 2: "Created 'filename' with the following content:\n{content}" (multiline format)
|
|
2484
|
+
const createMatch = messageText.match(/(?:Successfully created|Created file|Wrote|Created)\s+['"']?([^\s'"]+)['"']?\s+(?:with|containing)/i);
|
|
2485
|
+
if (createMatch) {
|
|
2486
|
+
const filepath = createMatch[1];
|
|
2487
|
+
|
|
2488
|
+
// Try to extract content after pattern
|
|
2489
|
+
// Look for content after "content:" or "following:" followed by newline and content
|
|
2490
|
+
const contentMatch = messageText.match(/(?:content|following):\s*\n([\s\S]+?)(?:\n\n|$)/i);
|
|
2491
|
+
if (contentMatch) {
|
|
2492
|
+
return {
|
|
2493
|
+
operation: 'create',
|
|
2494
|
+
path: filepath,
|
|
2495
|
+
content: contentMatch[1].trim(),
|
|
2496
|
+
language: this.detectLanguage(filepath)
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// Pattern 3: "Modified 'filename'" or "Updated 'filename'"
|
|
2502
|
+
const editMatch = messageText.match(/(?:Modified|Updated|Edited)\s+['"']([^'"']+)['"']/i);
|
|
2503
|
+
if (editMatch) {
|
|
2504
|
+
return {
|
|
2505
|
+
operation: 'edit',
|
|
2506
|
+
path: editMatch[1],
|
|
2507
|
+
language: this.detectLanguage(editMatch[1])
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
return null;
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/**
|
|
2515
|
+
* Detect programming language from filename
|
|
2516
|
+
*/
|
|
2517
|
+
detectLanguage(filename) {
|
|
2518
|
+
if (!filename) return 'text';
|
|
2519
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
2520
|
+
const langMap = {
|
|
2521
|
+
'js': 'javascript',
|
|
2522
|
+
'ts': 'typescript',
|
|
2523
|
+
'py': 'python',
|
|
2524
|
+
'jsx': 'javascript',
|
|
2525
|
+
'tsx': 'typescript',
|
|
2526
|
+
'json': 'json',
|
|
2527
|
+
'html': 'html',
|
|
2528
|
+
'css': 'css',
|
|
2529
|
+
'md': 'markdown',
|
|
2530
|
+
'sh': 'bash',
|
|
2531
|
+
'yaml': 'yaml',
|
|
2532
|
+
'yml': 'yaml',
|
|
2533
|
+
'xml': 'xml',
|
|
2534
|
+
'sql': 'sql',
|
|
2535
|
+
'txt': 'text',
|
|
2536
|
+
'rb': 'ruby',
|
|
2537
|
+
'go': 'go',
|
|
2538
|
+
'rs': 'rust',
|
|
2539
|
+
'java': 'java',
|
|
2540
|
+
'cpp': 'cpp',
|
|
2541
|
+
'c': 'c',
|
|
2542
|
+
'php': 'php'
|
|
2543
|
+
};
|
|
2544
|
+
return langMap[ext] || 'text';
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
/**
|
|
2548
|
+
* Render file preview with syntax highlighting
|
|
2549
|
+
*/
|
|
2550
|
+
renderFilePreview(fileOp) {
|
|
2551
|
+
const { operation, path, content, language } = fileOp;
|
|
2552
|
+
|
|
2553
|
+
// Icon based on operation
|
|
2554
|
+
const icon = operation === 'create' ? 'š' : 'āļø';
|
|
2555
|
+
|
|
2556
|
+
return `
|
|
2557
|
+
<div class="file-preview">
|
|
2558
|
+
<div class="file-preview-header">
|
|
2559
|
+
<span class="file-preview-icon">${icon}</span>
|
|
2560
|
+
<span class="file-preview-path">${escapeHtml(path)}</span>
|
|
2561
|
+
</div>
|
|
2562
|
+
${content ? `
|
|
2563
|
+
<pre class="file-preview-content"><code class="language-${language}">${escapeHtml(content)}</code></pre>
|
|
2564
|
+
` : ''}
|
|
2565
|
+
</div>
|
|
2566
|
+
`;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
renderMultiAgentRelationship(message) {
|
|
2570
|
+
const time = this._formatTime(message.timestamp);
|
|
2571
|
+
const agentList = message.agents
|
|
2572
|
+
.sort((a, b) => a.sequence_position - b.sequence_position)
|
|
2573
|
+
.map(agent => `${agent.name} (${agent.model})`)
|
|
2574
|
+
.join(' ā ');
|
|
2575
|
+
|
|
2576
|
+
return `
|
|
2577
|
+
<span class="message-time">[${time}]</span>
|
|
2578
|
+
<span class="message-author system">*</span>
|
|
2579
|
+
<span class="system-message-content">
|
|
2580
|
+
<span class="relationship-icon">š¤</span>
|
|
2581
|
+
Multi-agent session initialized:
|
|
2582
|
+
<span class="relationship-agents">${escapeHtml(agentList)}</span>
|
|
2583
|
+
</span>
|
|
2584
|
+
`;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
async sendMessage() {
|
|
2588
|
+
// Rate limiting - prevent rapid message sending
|
|
2589
|
+
const canSend = this.messageRateLimiter.call(() => {}, false);
|
|
2590
|
+
if (!canSend) {
|
|
2591
|
+
logger.debug('[Chat] Rate limited, message sending delayed');
|
|
2592
|
+
// Don't show error, just silently queue via rate limiter
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// SECURITY: Check if user is owner before allowing message send
|
|
2596
|
+
const isOwner = sessionState.isOwner;
|
|
2597
|
+
const isAuthenticated = sessionState.isAuthenticated;
|
|
2598
|
+
|
|
2599
|
+
if (!isOwner && !this.isNewSession) {
|
|
2600
|
+
logger.warn('[Chat] Attempted to send message without ownership');
|
|
2601
|
+
const message = isAuthenticated
|
|
2602
|
+
? "You cannot send messages to this session (you are not the owner)."
|
|
2603
|
+
: "Please sign in to interact with this session.";
|
|
2604
|
+
this.showError(message);
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
const text = this.inputField.value.trim();
|
|
2609
|
+
|
|
2610
|
+
if (!text) {
|
|
2611
|
+
return;
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// Disable input
|
|
2615
|
+
this.inputField.disabled = true;
|
|
2616
|
+
this.sendButton.disabled = true;
|
|
2617
|
+
|
|
2618
|
+
try {
|
|
2619
|
+
// If this is a new session, create it first
|
|
2620
|
+
if (this.isNewSession) {
|
|
2621
|
+
await this.createSessionAndSendMessage(text);
|
|
2622
|
+
} else {
|
|
2623
|
+
// Normal message send for existing session
|
|
2624
|
+
await this.sendMessageToExistingSession(text);
|
|
2625
|
+
}
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
logger.error('[Chat] Send error:', error);
|
|
2628
|
+
|
|
2629
|
+
// Reset button text if it's stuck on "Creating session..."
|
|
2630
|
+
if (this.sendButton.textContent !== 'Send') {
|
|
2631
|
+
this.sendButton.textContent = 'Send';
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
// Clean up any pending thinking message on error
|
|
2635
|
+
this.cleanupThinkingState();
|
|
2636
|
+
|
|
2637
|
+
// Show error with context based on status code
|
|
2638
|
+
if (error.status === 403) {
|
|
2639
|
+
this.showError(error.message, 'beta-access');
|
|
2640
|
+
} else if (error.status === 429) {
|
|
2641
|
+
this.showError(error.message || 'Rate limit exceeded. Please try again in a moment.', 'rate-limit');
|
|
2642
|
+
} else if (error.status === 402 || error.message?.includes('quota') || error.message?.includes('credits')) {
|
|
2643
|
+
this.showError(error.message || 'Insufficient credits. Please deposit funds.', 'quota');
|
|
2644
|
+
} else {
|
|
2645
|
+
this.showError(error.message, 'error');
|
|
2646
|
+
}
|
|
2647
|
+
} finally {
|
|
2648
|
+
// Re-enable input (only if owner - don't re-enable for non-owners)
|
|
2649
|
+
if (isOwner || this.isNewSession) {
|
|
2650
|
+
this.inputField.disabled = false;
|
|
2651
|
+
this.sendButton.disabled = false;
|
|
2652
|
+
this.inputField.focus();
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
async createSessionAndSendMessage(message) {
|
|
2658
|
+
logger.debug('[Chat] Creating new session with message:', message);
|
|
2659
|
+
|
|
2660
|
+
// Show loading state
|
|
2661
|
+
this.sendButton.textContent = 'Creating session...';
|
|
2662
|
+
|
|
2663
|
+
// Get auth token if exists
|
|
2664
|
+
const token = localStorage.getItem('auth_token');
|
|
2665
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
2666
|
+
if (token) {
|
|
2667
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// Check if we have pending files that need to be uploaded
|
|
2671
|
+
const hasPendingFiles = this.fileManager && this.fileManager.hasPendingFiles();
|
|
2672
|
+
logger.debug('[Chat] Has pending files:', hasPendingFiles);
|
|
2673
|
+
|
|
2674
|
+
// Get configuration from config panel if available
|
|
2675
|
+
let sessionConfig = {
|
|
2676
|
+
task: message, // Store task for display
|
|
2677
|
+
model: 'claude-sonnet-4.5', // Use Claude by default (Anthropic works reliably)
|
|
2678
|
+
tools: ['browser', 'filesystem'],
|
|
2679
|
+
max_steps: 50,
|
|
2680
|
+
temperature: 0.0,
|
|
2681
|
+
use_vision: true,
|
|
2682
|
+
auto_start: !hasPendingFiles, // NEW: Don't auto-start if files pending
|
|
2683
|
+
wait_for_uploads: hasPendingFiles // Signal backend to wait
|
|
2684
|
+
};
|
|
2685
|
+
|
|
2686
|
+
// If config panel exists, use its configuration
|
|
2687
|
+
if (window.configPanel) {
|
|
2688
|
+
try {
|
|
2689
|
+
const config = window.configPanel.getConfiguration();
|
|
2690
|
+
if (config) {
|
|
2691
|
+
sessionConfig = {
|
|
2692
|
+
task: message,
|
|
2693
|
+
...config,
|
|
2694
|
+
// CRITICAL FIX: Preserve auto_start logic based on pending files
|
|
2695
|
+
// Don't let config panel override this - files MUST upload first!
|
|
2696
|
+
auto_start: hasPendingFiles ? false : (config.auto_start !== undefined ? config.auto_start : true),
|
|
2697
|
+
// Also set wait_for_uploads to signal backend
|
|
2698
|
+
wait_for_uploads: hasPendingFiles
|
|
2699
|
+
};
|
|
2700
|
+
logger.debug('[Chat] Using config from panel:', sessionConfig);
|
|
2701
|
+
if (hasPendingFiles) {
|
|
2702
|
+
logger.debug('[Chat] š FORCING auto_start=false due to pending files');
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
} catch (error) {
|
|
2706
|
+
logger.warn('[Chat] Failed to get configuration from panel, using defaults:', error);
|
|
2707
|
+
// Fallback to defaults is already set in sessionConfig init
|
|
2708
|
+
this.showError(`Configuration error: ${error.message}. Using default settings.`);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// NO optimistic add - message will appear from session_start event in feed
|
|
2713
|
+
// This prevents duplicates (optimistic + feed echo)
|
|
2714
|
+
// Use pending queue for visual feedback instead
|
|
2715
|
+
this._addToPendingQueue(message);
|
|
2716
|
+
|
|
2717
|
+
// Show file attachment indicator if files are pending
|
|
2718
|
+
if (this.fileManager && this.fileManager.hasPendingFiles()) {
|
|
2719
|
+
const fileCount = this.fileManager.pendingFiles.size;
|
|
2720
|
+
const fileList = Array.from(this.fileManager.pendingFiles.values()).map(f => f.file.name).join(', ');
|
|
2721
|
+
this.addMessage(this._createMessage('system', `š Attaching ${fileCount} file(s): ${fileList}`));
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// Clear input immediately
|
|
2725
|
+
this.inputField.value = '';
|
|
2726
|
+
this.inputField.style.height = '28px'; // Reset height
|
|
2727
|
+
|
|
2728
|
+
// Create session with configuration (thinking message created after session exists)
|
|
2729
|
+
const response = await fetch('/api/task/sessions', {
|
|
2730
|
+
method: 'POST',
|
|
2731
|
+
headers: headers,
|
|
2732
|
+
credentials: 'include',
|
|
2733
|
+
body: JSON.stringify(sessionConfig)
|
|
2734
|
+
});
|
|
2735
|
+
|
|
2736
|
+
if (!response.ok) {
|
|
2737
|
+
// Parse error response - handle multiple formats
|
|
2738
|
+
let errorMessage = 'Failed to create session';
|
|
2739
|
+
let errorDetail = null;
|
|
2740
|
+
|
|
2741
|
+
try {
|
|
2742
|
+
const errorData = await response.json();
|
|
2743
|
+
// FastAPI returns {detail: "message"} or {detail: {error: "...", message: "..."}}
|
|
2744
|
+
if (typeof errorData.detail === 'string') {
|
|
2745
|
+
errorMessage = errorData.detail;
|
|
2746
|
+
} else if (errorData.detail && errorData.detail.message) {
|
|
2747
|
+
errorMessage = errorData.detail.message;
|
|
2748
|
+
errorDetail = errorData.detail;
|
|
2749
|
+
} else if (errorData.message) {
|
|
2750
|
+
errorMessage = errorData.message;
|
|
2751
|
+
}
|
|
2752
|
+
} catch (e) {
|
|
2753
|
+
// JSON parse failed - use status text
|
|
2754
|
+
errorMessage = response.statusText || 'Failed to create session';
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// Create error with additional context
|
|
2758
|
+
const error = new Error(errorMessage);
|
|
2759
|
+
error.status = response.status;
|
|
2760
|
+
error.detail = errorDetail;
|
|
2761
|
+
throw error;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
const data = await response.json();
|
|
2765
|
+
const newSessionId = data.session_id;
|
|
2766
|
+
|
|
2767
|
+
logger.debug('[Chat] Session created:', newSessionId);
|
|
2768
|
+
|
|
2769
|
+
// Upload files if any pending
|
|
2770
|
+
let uploadedFiles = [];
|
|
2771
|
+
if (this.fileManager && this.fileManager.hasPendingFiles()) {
|
|
2772
|
+
logger.debug('[Chat] Uploading pending files to new session workspace...');
|
|
2773
|
+
try {
|
|
2774
|
+
uploadedFiles = await this.fileManager.uploadAllFiles(newSessionId);
|
|
2775
|
+
logger.debug('[Chat] Files uploaded to workspace:', uploadedFiles);
|
|
2776
|
+
|
|
2777
|
+
// Clear file manager
|
|
2778
|
+
this.fileManager.clear();
|
|
2779
|
+
|
|
2780
|
+
// NOTE: File upload notification will come from backend feed event
|
|
2781
|
+
// with proper timestamp ordering. No need to add it here.
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
logger.error('[Chat] File upload failed:', error);
|
|
2784
|
+
this.addMessage(this._createMessage('system', `ā File upload failed: ${error.message}`));
|
|
2785
|
+
uploadedFiles = []; // Reset on error
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
// CRITICAL FIX: Send initial task as first message with attached files
|
|
2790
|
+
// This allows agent to start with files already uploaded (and triggers start if auto_start=false)
|
|
2791
|
+
if (uploadedFiles.length > 0) {
|
|
2792
|
+
logger.debug('[Chat] Sending initial task as message with attached files to trigger start...');
|
|
2793
|
+
|
|
2794
|
+
// Wait a moment for session to be fully initialized
|
|
2795
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2796
|
+
|
|
2797
|
+
const msgResponse = await fetch(`/api/session/${newSessionId}/messages`, {
|
|
2798
|
+
method: 'POST',
|
|
2799
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2800
|
+
body: JSON.stringify({
|
|
2801
|
+
text: message,
|
|
2802
|
+
kind: 'guidance',
|
|
2803
|
+
metadata: {},
|
|
2804
|
+
attached_files: uploadedFiles
|
|
2805
|
+
})
|
|
2806
|
+
});
|
|
2807
|
+
|
|
2808
|
+
if (!msgResponse.ok) {
|
|
2809
|
+
const errorText = await msgResponse.text();
|
|
2810
|
+
logger.error('[Chat] Failed to send initial message with files:', errorText);
|
|
2811
|
+
throw new Error(`Failed to send message with files: ${msgResponse.status}`);
|
|
2812
|
+
} else {
|
|
2813
|
+
logger.debug('[Chat] ā Initial task sent with attached files - session starting now');
|
|
2814
|
+
// Show thinking message after successful message send
|
|
2815
|
+
this.createImmediateThinkingMessage();
|
|
2816
|
+
}
|
|
2817
|
+
// With files: user_message event will come from backend, skipHistory OK
|
|
2818
|
+
await this.transitionToActiveSession(newSessionId, { skipHistory: true });
|
|
2819
|
+
} else {
|
|
2820
|
+
// No files - session will auto-start, show thinking message
|
|
2821
|
+
logger.debug('[Chat] No files attached - session will auto-start');
|
|
2822
|
+
this.createImmediateThinkingMessage();
|
|
2823
|
+
// Without files: need to load history to get initial task from /task API
|
|
2824
|
+
await this.transitionToActiveSession(newSessionId, { skipHistory: false });
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
// Clear pending queue - session is now active and task is displayed
|
|
2828
|
+
// (either from /task API for no-files, or will come via user_message event for files)
|
|
2829
|
+
this.pendingMessages = [];
|
|
2830
|
+
this._updateQueueIndicator(false);
|
|
2831
|
+
|
|
2832
|
+
// Reset button
|
|
2833
|
+
this.sendButton.textContent = 'Send';
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
async transitionToActiveSession(sessionId, options = {}) {
|
|
2837
|
+
logger.debug('[Chat] Transitioning to active session:', sessionId, 'options:', options);
|
|
2838
|
+
|
|
2839
|
+
// 1. Use centralized session state - this handles URL update and DOM attributes
|
|
2840
|
+
sessionState.transitionToActive(sessionId);
|
|
2841
|
+
|
|
2842
|
+
// 2. Update internal state (for backward compatibility)
|
|
2843
|
+
this.sessionId = sessionId;
|
|
2844
|
+
this.isNewSession = false;
|
|
2845
|
+
|
|
2846
|
+
// 4. Hide config panel if it exists
|
|
2847
|
+
if (window.configPanel) {
|
|
2848
|
+
window.configPanel.hide();
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
// 5. Switch from empty to active state in UI
|
|
2852
|
+
this.activateEmptyStateElements();
|
|
2853
|
+
|
|
2854
|
+
// 5. Trigger WebSocket initialization
|
|
2855
|
+
// Wait a moment for DOM attributes to update
|
|
2856
|
+
setTimeout(() => {
|
|
2857
|
+
if (window.triggerSocketInitialization) {
|
|
2858
|
+
logger.debug('[Chat] Triggering socket initialization');
|
|
2859
|
+
window.triggerSocketInitialization();
|
|
2860
|
+
} else {
|
|
2861
|
+
logger.warn('[Chat] triggerSocketInitialization not available');
|
|
2862
|
+
}
|
|
2863
|
+
}, 100);
|
|
2864
|
+
|
|
2865
|
+
// 6. Update sidebar with session ID
|
|
2866
|
+
const sessionIdDisplay = document.getElementById('session-id');
|
|
2867
|
+
if (sessionIdDisplay) {
|
|
2868
|
+
sessionIdDisplay.textContent = sessionId.substring(0, 8) + '...';
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
const sessionStatus = document.getElementById('session-status');
|
|
2872
|
+
if (sessionStatus) {
|
|
2873
|
+
sessionStatus.textContent = 'Initializing';
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// 7. Wait for socket to be ready and setup chat listeners
|
|
2877
|
+
if (window.socketReady) {
|
|
2878
|
+
try {
|
|
2879
|
+
logger.debug('[Chat] Waiting for socket to be ready...');
|
|
2880
|
+
const socket = await window.socketReady;
|
|
2881
|
+
this.socket = socket;
|
|
2882
|
+
this.setupSocketListeners();
|
|
2883
|
+
logger.debug('[Chat] Chat socket listeners setup complete');
|
|
2884
|
+
} catch (err) {
|
|
2885
|
+
logger.error('[Chat] Failed to setup socket listeners:', err);
|
|
2886
|
+
}
|
|
2887
|
+
} else {
|
|
2888
|
+
logger.warn('[Chat] window.socketReady not available');
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
// 8. Load chat history (skip if coming from new session creation - we already have the message)
|
|
2892
|
+
if (!options.skipHistory) {
|
|
2893
|
+
logger.debug('[Chat] Loading chat history...');
|
|
2894
|
+
await this.loadHistory();
|
|
2895
|
+
} else {
|
|
2896
|
+
logger.debug('[Chat] Skipping history load (messages already added optimistically)');
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
logger.debug('[Chat] Transition complete');
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
activateEmptyStateElements() {
|
|
2903
|
+
logger.debug('[Chat] Activating empty state elements');
|
|
2904
|
+
|
|
2905
|
+
// Hide all "when-empty" elements
|
|
2906
|
+
document.querySelectorAll('.when-empty').forEach(el => {
|
|
2907
|
+
el.style.display = 'none';
|
|
2908
|
+
});
|
|
2909
|
+
|
|
2910
|
+
// Show all "when-active" elements
|
|
2911
|
+
document.querySelectorAll('.when-active').forEach(el => {
|
|
2912
|
+
el.style.display = '';
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
async sendMessageToExistingSession(text) {
|
|
2917
|
+
// Upload files if any pending
|
|
2918
|
+
let attachedFiles = [];
|
|
2919
|
+
if (this.fileManager && this.fileManager.hasPendingFiles()) {
|
|
2920
|
+
logger.debug('[Chat] Uploading pending files before sending message...');
|
|
2921
|
+
try {
|
|
2922
|
+
attachedFiles = await this.fileManager.uploadAllFiles(this.sessionId);
|
|
2923
|
+
logger.debug('[Chat] Files uploaded successfully:', attachedFiles);
|
|
2924
|
+
|
|
2925
|
+
// NOTE: File upload notification will come from backend feed event
|
|
2926
|
+
// with proper timestamp ordering. No need to add it here.
|
|
2927
|
+
} catch (error) {
|
|
2928
|
+
logger.error('[Chat] File upload failed:', error);
|
|
2929
|
+
this.addMessage(this._createMessage('system', `ā File upload failed: ${error.message}`));
|
|
2930
|
+
this.showError('Failed to upload files. Please try again.');
|
|
2931
|
+
throw error; // Stop message send if files fail to upload
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// NO optimistic update - message will appear from feed event
|
|
2936
|
+
// This prevents duplicates (optimistic + feed echo)
|
|
2937
|
+
|
|
2938
|
+
// Clear input immediately for responsiveness
|
|
2939
|
+
this.inputField.value = '';
|
|
2940
|
+
this.inputField.style.height = '28px'; // Reset height
|
|
2941
|
+
|
|
2942
|
+
// Clear attached files (they're now uploaded)
|
|
2943
|
+
if (this.fileManager) {
|
|
2944
|
+
this.fileManager.clear();
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// Add to pending queue display (will be removed when feed event arrives)
|
|
2948
|
+
this._addToPendingQueue(text);
|
|
2949
|
+
|
|
2950
|
+
// Send to API
|
|
2951
|
+
const response = await fetch(`/api/session/${this.sessionId}/messages`, {
|
|
2952
|
+
method: 'POST',
|
|
2953
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2954
|
+
body: JSON.stringify({
|
|
2955
|
+
text: text,
|
|
2956
|
+
kind: 'comment',
|
|
2957
|
+
metadata: {},
|
|
2958
|
+
attached_files: attachedFiles.length > 0 ? attachedFiles : undefined
|
|
2959
|
+
})
|
|
2960
|
+
});
|
|
2961
|
+
|
|
2962
|
+
if (!response.ok) {
|
|
2963
|
+
throw new Error(`HTTP ${response.status}`);
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
logger.debug('[Chat] Message sent successfully', attachedFiles.length > 0 ? `with ${attachedFiles.length} file(s)` : '');
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
updateStreamingContent(chunk) {
|
|
2970
|
+
// Find the current agent message element
|
|
2971
|
+
if (!this.currentAgentMessage) {
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
const msgEl = document.querySelector(`[data-message-id="${this.currentAgentMessage.id}"]`);
|
|
2976
|
+
if (!msgEl) {
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
const thinkingContent = msgEl.querySelector('.thinking-content');
|
|
2981
|
+
if (thinkingContent) {
|
|
2982
|
+
thinkingContent.textContent += chunk;
|
|
2983
|
+
|
|
2984
|
+
// Auto-scroll if at bottom
|
|
2985
|
+
if (!this.scrollLocked) {
|
|
2986
|
+
this.scrollToBottom(true);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
scrollToBottom(smooth = false) {
|
|
2992
|
+
this.chatContainer.scrollTo({
|
|
2993
|
+
top: this.chatContainer.scrollHeight,
|
|
2994
|
+
behavior: smooth ? 'smooth' : 'auto'
|
|
2995
|
+
});
|
|
2996
|
+
|
|
2997
|
+
// Hide scroll button
|
|
2998
|
+
if (this.scrollButton) {
|
|
2999
|
+
this.scrollButton.style.display = 'none';
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
onScroll() {
|
|
3004
|
+
// Check if user has scrolled up
|
|
3005
|
+
const isAtBottom = (
|
|
3006
|
+
this.chatContainer.scrollHeight - this.chatContainer.scrollTop
|
|
3007
|
+
) <= (this.chatContainer.clientHeight + 50); // 50px threshold
|
|
3008
|
+
|
|
3009
|
+
this.scrollLocked = !isAtBottom;
|
|
3010
|
+
|
|
3011
|
+
// Show/hide scroll button
|
|
3012
|
+
if (this.scrollButton) {
|
|
3013
|
+
this.scrollButton.style.display = isAtBottom ? 'none' : 'block';
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
showLoadingState(message) {
|
|
3018
|
+
// Hide empty state
|
|
3019
|
+
if (this.emptyState) {
|
|
3020
|
+
this.emptyState.style.display = 'none';
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// Show loading message
|
|
3024
|
+
this.addMessage(this._createMessage('system', message, { id: 'msg_loading' }));
|
|
3025
|
+
}
|
|
3026
|
+
|
|
3027
|
+
hideLoadingState() {
|
|
3028
|
+
// Remove loading message if it exists
|
|
3029
|
+
const loadingMsg = document.querySelector('[data-message-id="msg_loading"]');
|
|
3030
|
+
if (loadingMsg) {
|
|
3031
|
+
loadingMsg.remove();
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
showError(message, errorType = 'error') {
|
|
3036
|
+
// Remove existing emoji from message to avoid duplicates
|
|
3037
|
+
const cleanMessage = message.replace(/^[ā ļøāšš°ā±ļø\s]+/, '');
|
|
3038
|
+
|
|
3039
|
+
// Choose emoji and add helpful context based on error type
|
|
3040
|
+
let emoji = 'ā ļø';
|
|
3041
|
+
let helpText = '';
|
|
3042
|
+
|
|
3043
|
+
switch (errorType) {
|
|
3044
|
+
case 'beta-access':
|
|
3045
|
+
emoji = 'š';
|
|
3046
|
+
helpText = '\n\nš” Beta access open for DEN holders. Get full instructions at t.me/tmachinrobot';
|
|
3047
|
+
break;
|
|
3048
|
+
case 'quota':
|
|
3049
|
+
emoji = 'š°';
|
|
3050
|
+
helpText = '\n\nš” Tip: Check your Profile to view balance and deposit address';
|
|
3051
|
+
break;
|
|
3052
|
+
case 'rate-limit':
|
|
3053
|
+
emoji = 'ā±ļø';
|
|
3054
|
+
break;
|
|
3055
|
+
case 'error':
|
|
3056
|
+
default:
|
|
3057
|
+
emoji = 'ā';
|
|
3058
|
+
break;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
this.addMessage(this._createMessage('system', `${emoji} ${cleanMessage}${helpText}`));
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
/**
|
|
3065
|
+
* Clean up all event listeners and resources.
|
|
3066
|
+
* Call this before destroying the ChatManager instance.
|
|
3067
|
+
*/
|
|
3068
|
+
destroy() {
|
|
3069
|
+
if (this._destroyed) {
|
|
3070
|
+
logger.debug('[Chat] Already destroyed, skipping');
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
logger.debug('[Chat] Destroying ChatManager, cleaning up resources...');
|
|
3075
|
+
|
|
3076
|
+
// Remove DOM event listeners
|
|
3077
|
+
if (this.sendButton && this._boundHandlers.sendClick) {
|
|
3078
|
+
this.sendButton.removeEventListener('click', this._boundHandlers.sendClick);
|
|
3079
|
+
}
|
|
3080
|
+
if (this.inputField) {
|
|
3081
|
+
if (this._boundHandlers.inputKeydown) {
|
|
3082
|
+
this.inputField.removeEventListener('keydown', this._boundHandlers.inputKeydown);
|
|
3083
|
+
}
|
|
3084
|
+
if (this._boundHandlers.inputPaste) {
|
|
3085
|
+
this.inputField.removeEventListener('paste', this._boundHandlers.inputPaste);
|
|
3086
|
+
}
|
|
3087
|
+
if (this._boundHandlers.inputChange) {
|
|
3088
|
+
this.inputField.removeEventListener('input', this._boundHandlers.inputChange);
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
if (this.chatContainer) {
|
|
3092
|
+
if (this._boundHandlers.scrollThrottled) {
|
|
3093
|
+
this.chatContainer.removeEventListener('scroll', this._boundHandlers.scrollThrottled);
|
|
3094
|
+
}
|
|
3095
|
+
if (this._boundHandlers.containerClick) {
|
|
3096
|
+
this.chatContainer.removeEventListener('click', this._boundHandlers.containerClick);
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
if (this.scrollButton && this._boundHandlers.scrollButtonClick) {
|
|
3100
|
+
this.scrollButton.removeEventListener('click', this._boundHandlers.scrollButtonClick);
|
|
3101
|
+
}
|
|
3102
|
+
if (this._boundHandlers.documentKeydown) {
|
|
3103
|
+
document.removeEventListener('keydown', this._boundHandlers.documentKeydown);
|
|
3104
|
+
}
|
|
3105
|
+
if (this._boundHandlers.viewportResize && window.visualViewport) {
|
|
3106
|
+
window.visualViewport.removeEventListener('resize', this._boundHandlers.viewportResize);
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
// Remove socket event listeners
|
|
3110
|
+
if (this.socket) {
|
|
3111
|
+
this.socket.off('stream_chunk');
|
|
3112
|
+
this.socket.off('streaming_output');
|
|
3113
|
+
this.socket.off('stream_update');
|
|
3114
|
+
this.socket.off('feed_update');
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// Unsubscribe from EventStore (CRITICAL for cleanup)
|
|
3118
|
+
if (this._eventStoreUnsubscribe) {
|
|
3119
|
+
this._eventStoreUnsubscribe();
|
|
3120
|
+
this._eventStoreUnsubscribe = null;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
// Clear event bus listeners (with proper handler references)
|
|
3124
|
+
if (this._boundHandlers.tabActivated) {
|
|
3125
|
+
off('tab:activated', this._boundHandlers.tabActivated);
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
// Clear state
|
|
3129
|
+
this.messages = [];
|
|
3130
|
+
this.currentAgentMessage = null;
|
|
3131
|
+
this._processedEventIds.clear();
|
|
3132
|
+
this._boundHandlers = {};
|
|
3133
|
+
this._destroyed = true;
|
|
3134
|
+
|
|
3135
|
+
logger.debug('[Chat] ChatManager destroyed');
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
/**
|
|
3139
|
+
* Trim messages array to prevent unbounded memory growth.
|
|
3140
|
+
* Called after adding messages.
|
|
3141
|
+
*/
|
|
3142
|
+
_trimMessages() {
|
|
3143
|
+
if (this.messages.length > ChatManager.MAX_MESSAGES) {
|
|
3144
|
+
const excess = this.messages.length - ChatManager.MAX_MESSAGES;
|
|
3145
|
+
this.messages = this.messages.slice(excess);
|
|
3146
|
+
logger.debug(`[Chat] Trimmed ${excess} old messages, now ${this.messages.length}`);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// EventMapper class removed - now handling events directly in ChatManager
|
|
3152
|
+
|
|
3153
|
+
// Initialize chat manager when DOM is ready
|
|
3154
|
+
let chatManager = null;
|
|
3155
|
+
|
|
3156
|
+
async function initializeChatManager() {
|
|
3157
|
+
// Use centralized session state
|
|
3158
|
+
const sessionId = sessionState.sessionId || 'new';
|
|
3159
|
+
|
|
3160
|
+
logger.debug('[Chat] Initializing - sessionId:', sessionId);
|
|
3161
|
+
|
|
3162
|
+
// Create manager immediately, it will handle socket connection internally
|
|
3163
|
+
chatManager = new ChatManager(sessionId);
|
|
3164
|
+
logger.debug('[Chat] Manager initialized');
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
// Wait for DOM to be ready
|
|
3168
|
+
logger.debug('[Chat] Module loading, readyState:', document.readyState);
|
|
3169
|
+
|
|
3170
|
+
if (document.readyState === 'loading') {
|
|
3171
|
+
document.addEventListener('DOMContentLoaded', initializeChatManager);
|
|
3172
|
+
} else {
|
|
3173
|
+
initializeChatManager();
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
logger.debug('[Chat] Module loaded');
|
|
3177
|
+
|
|
3178
|
+
export { chatManager };
|