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.
Files changed (745) hide show
  1. agents/README.md +453 -0
  2. agents/__init__.py +164 -0
  3. agents/base_agent.py +743 -0
  4. agents/personality/character.py +119 -0
  5. agents/personality/character_manager.py +309 -0
  6. agents/personality/characters/rob.character.json +56 -0
  7. agents/personality/characters/trump.character.json +63 -0
  8. agents/personality/persona_render.py +78 -0
  9. agents/personality/persona_resolver.py +115 -0
  10. agents/prompt/__init__.py +58 -0
  11. agents/prompt/base_prompt.py +92 -0
  12. agents/prompt/system.py +417 -0
  13. agents/task/README_PATH_MANAGEMENT.md +141 -0
  14. agents/task/__init__.py +162 -0
  15. agents/task/agent/__init__.py +70 -0
  16. agents/task/agent/agent_state.py +408 -0
  17. agents/task/agent/async_delegation.py +274 -0
  18. agents/task/agent/autonomy_state.py +275 -0
  19. agents/task/agent/conversation.py +75 -0
  20. agents/task/agent/core/__init__.py +6 -0
  21. agents/task/agent/core/aux_metering.py +61 -0
  22. agents/task/agent/core/background_review.py +157 -0
  23. agents/task/agent/core/construction.py +1179 -0
  24. agents/task/agent/core/conversational_exit.py +49 -0
  25. agents/task/agent/core/correspondent_gate.py +227 -0
  26. agents/task/agent/core/curator.py +246 -0
  27. agents/task/agent/core/episodic_digest.py +103 -0
  28. agents/task/agent/core/error_recovery.py +480 -0
  29. agents/task/agent/core/history_io.py +257 -0
  30. agents/task/agent/core/llm_provisioning.py +287 -0
  31. agents/task/agent/core/llm_runner.py +547 -0
  32. agents/task/agent/core/logging_io.py +276 -0
  33. agents/task/agent/core/loop_detection.py +262 -0
  34. agents/task/agent/core/memory_prefetch.py +325 -0
  35. agents/task/agent/core/memory_writer.py +667 -0
  36. agents/task/agent/core/model_introspection.py +82 -0
  37. agents/task/agent/core/model_swap.py +177 -0
  38. agents/task/agent/core/next_action_internal.py +1331 -0
  39. agents/task/agent/core/output_validation.py +303 -0
  40. agents/task/agent/core/project_context.py +272 -0
  41. agents/task/agent/core/resources.py +93 -0
  42. agents/task/agent/core/result_processing.py +389 -0
  43. agents/task/agent/core/run_loop.py +661 -0
  44. agents/task/agent/core/safety_lifecycle.py +228 -0
  45. agents/task/agent/core/secret_guard.py +293 -0
  46. agents/task/agent/core/self_wake.py +280 -0
  47. agents/task/agent/core/session_metadata.py +104 -0
  48. agents/task/agent/core/step.py +781 -0
  49. agents/task/agent/core/step_execution.py +398 -0
  50. agents/task/agent/core/step_telemetry.py +222 -0
  51. agents/task/agent/core/test_aux_metering.py +50 -0
  52. agents/task/agent/core/test_billing_failover.py +68 -0
  53. agents/task/agent/core/test_fatal_error_classifier.py +38 -0
  54. agents/task/agent/core/test_judge_validation.py +275 -0
  55. agents/task/agent/core/test_model_swap.py +190 -0
  56. agents/task/agent/core/test_reflection_offload.py +54 -0
  57. agents/task/agent/core/turn_input.py +8 -0
  58. agents/task/agent/core/untrusted_wrap.py +103 -0
  59. agents/task/agent/core/user_ingress.py +228 -0
  60. agents/task/agent/hitl_manager.py +396 -0
  61. agents/task/agent/log_sanitize.py +36 -0
  62. agents/task/agent/message_manager/config.py +97 -0
  63. agents/task/agent/message_manager/service.py +758 -0
  64. agents/task/agent/message_manager/tests.py +305 -0
  65. agents/task/agent/message_manager/tool_call_builder.py +539 -0
  66. agents/task/agent/message_manager/tool_message_repair.py +364 -0
  67. agents/task/agent/message_manager/views.py +143 -0
  68. agents/task/agent/messages/__init__.py +8 -0
  69. agents/task/agent/messages/builders.py +309 -0
  70. agents/task/agent/messages/compactor.py +611 -0
  71. agents/task/agent/messages/context_references.py +271 -0
  72. agents/task/agent/messages/filters.py +333 -0
  73. agents/task/agent/messages/guidance.py +328 -0
  74. agents/task/agent/messages/persistence.py +448 -0
  75. agents/task/agent/messages/retrieval.py +506 -0
  76. agents/task/agent/messages/sqlite_persistence.py +85 -0
  77. agents/task/agent/messages/test_compaction_upgrade.py +349 -0
  78. agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
  79. agents/task/agent/messages/test_sqlite_persistence.py +54 -0
  80. agents/task/agent/messages/token_counter.py +532 -0
  81. agents/task/agent/orchestrator.py +776 -0
  82. agents/task/agent/profile_manager.py +105 -0
  83. agents/task/agent/profile_registry.py +173 -0
  84. agents/task/agent/prompts.py +1090 -0
  85. agents/task/agent/scenario_registry.py +226 -0
  86. agents/task/agent/service.py +509 -0
  87. agents/task/agent/session.py +984 -0
  88. agents/task/agent/skill_discovery.py +110 -0
  89. agents/task/agent/skill_frontmatter.py +94 -0
  90. agents/task/agent/skill_manager.py +1201 -0
  91. agents/task/agent/skill_store.py +627 -0
  92. agents/task/agent/skill_validation.py +56 -0
  93. agents/task/agent/skill_writer.py +492 -0
  94. agents/task/agent/sub_agent_manager.py +1137 -0
  95. agents/task/agent/test_session_disk_recovery.py +34 -0
  96. agents/task/agent/tests.py +513 -0
  97. agents/task/agent/tool_call_tracker.py +511 -0
  98. agents/task/agent/views.py +469 -0
  99. agents/task/config.py +432 -0
  100. agents/task/constants.py +1384 -0
  101. agents/task/flag_defaults.py +63 -0
  102. agents/task/goals/__init__.py +1 -0
  103. agents/task/goals/autonomy_marker.py +28 -0
  104. agents/task/goals/board.py +690 -0
  105. agents/task/goals/completion_judge.py +247 -0
  106. agents/task/goals/context.py +62 -0
  107. agents/task/goals/dispatcher.py +848 -0
  108. agents/task/goals/escalation.py +102 -0
  109. agents/task/goals/planner.py +114 -0
  110. agents/task/logging_config.py +155 -0
  111. agents/task/path.py +991 -0
  112. agents/task/robust_parse_config.py +400 -0
  113. agents/task/runtime/__init__.py +0 -0
  114. agents/task/runtime/run_as_session.py +130 -0
  115. agents/task/runtime_safety.py +308 -0
  116. agents/task/session/__init__.py +5 -0
  117. agents/task/session/browser_pool.py +130 -0
  118. agents/task/session/cleanup.py +612 -0
  119. agents/task/session/execution.py +397 -0
  120. agents/task/session/feed.py +200 -0
  121. agents/task/session/hitl_ingress.py +201 -0
  122. agents/task/session/hooks.py +83 -0
  123. agents/task/session/multi_agent.py +243 -0
  124. agents/task/session/workspace.py +91 -0
  125. agents/task/session_registry.py +91 -0
  126. agents/task/session_route.py +35 -0
  127. agents/task/sqlite_session_registry.py +261 -0
  128. agents/task/surface_config.py +185 -0
  129. agents/task/telemetry/__init__.py +58 -0
  130. agents/task/telemetry/event_log.py +180 -0
  131. agents/task/telemetry/formatters.py +642 -0
  132. agents/task/telemetry/manager.py +571 -0
  133. agents/task/telemetry/memory_events.py +66 -0
  134. agents/task/telemetry/self_events.py +53 -0
  135. agents/task/telemetry/sequence.py +160 -0
  136. agents/task/telemetry/service.py +1265 -0
  137. agents/task/telemetry/views.py +882 -0
  138. agents/task/templates.py +98 -0
  139. agents/task/test_aux_chain.py +151 -0
  140. agents/task/test_aux_router.py +41 -0
  141. agents/task/tests/test_parse_robustness.py +170 -0
  142. agents/task/tool_defaults.py +110 -0
  143. agents/task/utils.py +762 -0
  144. agents/task/utils_json.py +768 -0
  145. agents/task/utils_webview.py +86 -0
  146. agents/task/workspace_context.py +229 -0
  147. agents/task_agent_lite.py +2398 -0
  148. api/README.md +683 -0
  149. api/TASK_API_DOCS.md +151 -0
  150. api/__init__.py +0 -0
  151. api/a2a/__init__.py +37 -0
  152. api/a2a/agent_card.py +337 -0
  153. api/a2a/client.py +657 -0
  154. api/a2a/endpoints.py +375 -0
  155. api/a2a/models.py +268 -0
  156. api/a2a/streaming.py +419 -0
  157. api/a2a/task_handler.py +929 -0
  158. api/admin_endpoints.py +1584 -0
  159. api/app.py +923 -0
  160. api/auth_constants.py +28 -0
  161. api/auth_endpoints.py +402 -0
  162. api/auth_state.py +36 -0
  163. api/chat_via_task.py +42 -0
  164. api/conversation_manager.py +231 -0
  165. api/dependencies.py +186 -0
  166. api/eip8004_endpoints.py +452 -0
  167. api/hyperliquid_models.py +174 -0
  168. api/hyperliquid_routes.py +450 -0
  169. api/interfaces.py +207 -0
  170. api/jwt_middleware.py +83 -0
  171. api/kb/__init__.py +1 -0
  172. api/kb/endpoints.py +314 -0
  173. api/mcp_models.py +279 -0
  174. api/mcp_routes.py +438 -0
  175. api/middleware.py +526 -0
  176. api/models.py +176 -0
  177. api/openai_compat/__init__.py +0 -0
  178. api/openai_compat/model_map.py +25 -0
  179. api/openai_compat/models.py +59 -0
  180. api/openai_compat/router.py +140 -0
  181. api/payment_endpoints.py +444 -0
  182. api/payment_verification.py +138 -0
  183. api/polymarket_models.py +155 -0
  184. api/polymarket_routes.py +501 -0
  185. api/pricing_endpoints.py +89 -0
  186. api/session_routing.py +54 -0
  187. api/skill_endpoints.py +453 -0
  188. api/task_http_api.py +1856 -0
  189. api/webhooks.py +52 -0
  190. api/x402_endpoints.py +280 -0
  191. avatar/README.md +44 -0
  192. avatar/__init__.py +1 -0
  193. avatar/config/rob.json +7 -0
  194. avatar/mindprint.js +395 -0
  195. avatar/renders/rob.meta.json +20 -0
  196. avatar/renders/rob.png +0 -0
  197. avatar/studio.html +373 -0
  198. avatar/webview/avatar-live.js +44 -0
  199. cli/README.md +89 -0
  200. cli/__init__.py +0 -0
  201. cli/cli_surface.py +67 -0
  202. cli/commands/__init__.py +0 -0
  203. cli/commands/_bootstrap.py +49 -0
  204. cli/commands/_errors.py +53 -0
  205. cli/commands/chat.py +973 -0
  206. cli/commands/config.py +100 -0
  207. cli/commands/dashboard.py +103 -0
  208. cli/commands/doctor.py +222 -0
  209. cli/commands/email.py +174 -0
  210. cli/commands/gateway.py +346 -0
  211. cli/commands/goals.py +509 -0
  212. cli/commands/init.py +276 -0
  213. cli/commands/journey.py +30 -0
  214. cli/commands/kb.py +280 -0
  215. cli/commands/model.py +107 -0
  216. cli/commands/owner.py +431 -0
  217. cli/commands/pfp.py +232 -0
  218. cli/commands/run.py +414 -0
  219. cli/commands/serve.py +41 -0
  220. cli/commands/session.py +684 -0
  221. cli/commands/skill_install.py +786 -0
  222. cli/commands/skills.py +128 -0
  223. cli/commands/subagents.py +100 -0
  224. cli/commands/surface.py +81 -0
  225. cli/commands/telegram.py +185 -0
  226. cli/commands/todos.py +209 -0
  227. cli/commands/tools.py +102 -0
  228. cli/commands/update.py +316 -0
  229. cli/commands/whatsapp.py +175 -0
  230. cli/config_store.py +276 -0
  231. cli/gitignore.py +43 -0
  232. cli/inventory.py +46 -0
  233. cli/keys.py +101 -0
  234. cli/persona.py +17 -0
  235. cli/polyrob.py +137 -0
  236. cli/ui/__init__.py +64 -0
  237. cli/ui/activity.py +141 -0
  238. cli/ui/app.py +538 -0
  239. cli/ui/banner.py +233 -0
  240. cli/ui/blocks.py +534 -0
  241. cli/ui/bootstrap_notice.py +47 -0
  242. cli/ui/commands/__init__.py +41 -0
  243. cli/ui/commands/h_cron.py +64 -0
  244. cli/ui/commands/h_journey.py +164 -0
  245. cli/ui/commands/h_kb.py +121 -0
  246. cli/ui/commands/h_learn.py +100 -0
  247. cli/ui/commands/h_mcp.py +215 -0
  248. cli/ui/commands/h_self.py +113 -0
  249. cli/ui/commands/h_skills.py +314 -0
  250. cli/ui/commands/handlers.py +1672 -0
  251. cli/ui/commands/registry.py +426 -0
  252. cli/ui/dialog.py +425 -0
  253. cli/ui/event_registry.py +110 -0
  254. cli/ui/event_specs.py +45 -0
  255. cli/ui/events.py +433 -0
  256. cli/ui/identity.py +15 -0
  257. cli/ui/lifecycle.py +159 -0
  258. cli/ui/live_hooks.py +43 -0
  259. cli/ui/model_selector.py +532 -0
  260. cli/ui/persistent_loop.py +181 -0
  261. cli/ui/pick.py +113 -0
  262. cli/ui/plain_renderer.py +351 -0
  263. cli/ui/renderer.py +384 -0
  264. cli/ui/rich_renderer.py +533 -0
  265. cli/ui/secrets.py +115 -0
  266. cli/ui/state.py +376 -0
  267. cli/ui/statusbar.py +231 -0
  268. cli/ui/streaming.py +254 -0
  269. cli/ui/terminal_render.py +105 -0
  270. cli/ui/theme.py +132 -0
  271. cli/update/__init__.py +6 -0
  272. cli/update/context.py +121 -0
  273. cli/update/detect.py +176 -0
  274. cli/update/engine.py +95 -0
  275. cli/update/migrate_guarded.py +55 -0
  276. cli/update/process_guard.py +215 -0
  277. cli/update/runners.py +78 -0
  278. cli/update/snapshot.py +270 -0
  279. cli/update/versions.py +205 -0
  280. core/README.md +377 -0
  281. core/__init__.py +174 -0
  282. core/assets.py +87 -0
  283. core/async_bridge.py +76 -0
  284. core/autonomy_runtime.py +259 -0
  285. core/base_component.py +302 -0
  286. core/bootstrap.py +681 -0
  287. core/bot.py +310 -0
  288. core/config.py +833 -0
  289. core/constants.py +126 -0
  290. core/container.py +462 -0
  291. core/db_manifest.py +101 -0
  292. core/embedding.py +57 -0
  293. core/env.py +39 -0
  294. core/exceptions.py +287 -0
  295. core/flags.py +144 -0
  296. core/flags_catalog.py +345 -0
  297. core/home_migration.py +62 -0
  298. core/identity.py +118 -0
  299. core/initialization.py +1004 -0
  300. core/instance.py +537 -0
  301. core/interactive_gate.py +96 -0
  302. core/logging.py +436 -0
  303. core/owner_doc_writer.py +176 -0
  304. core/pairing.py +193 -0
  305. core/path_safety.py +20 -0
  306. core/paths.py +27 -0
  307. core/payment_config.py +21 -0
  308. core/permissions.py +322 -0
  309. core/runtime_config.py +96 -0
  310. core/runtime_paths.py +135 -0
  311. core/seams.py +35 -0
  312. core/secret_scan.py +25 -0
  313. core/secret_scrub.py +71 -0
  314. core/secrets.py +35 -0
  315. core/security_logging_filter.py +148 -0
  316. core/self_context_writer.py +333 -0
  317. core/self_evolution.py +294 -0
  318. core/session_context.py +47 -0
  319. core/sqlite_util.py +59 -0
  320. core/surfaces/__init__.py +16 -0
  321. core/surfaces/access.py +118 -0
  322. core/surfaces/binding.py +88 -0
  323. core/surfaces/bootstrap.py +78 -0
  324. core/surfaces/circuit.py +168 -0
  325. core/surfaces/continuity.py +77 -0
  326. core/surfaces/correspondents.py +228 -0
  327. core/surfaces/dispatcher.py +216 -0
  328. core/surfaces/envelopes.py +75 -0
  329. core/surfaces/gc.py +30 -0
  330. core/surfaces/idempotency.py +57 -0
  331. core/surfaces/inbound_webhook.py +124 -0
  332. core/surfaces/media.py +32 -0
  333. core/surfaces/message_router.py +91 -0
  334. core/surfaces/outbound_allowlist.py +80 -0
  335. core/surfaces/outbound_dispatcher.py +110 -0
  336. core/surfaces/outbound_mirror.py +37 -0
  337. core/surfaces/outbound_queue.py +107 -0
  338. core/surfaces/outbound_target.py +12 -0
  339. core/surfaces/owner_admin.py +29 -0
  340. core/surfaces/proactive.py +78 -0
  341. core/surfaces/progress.py +108 -0
  342. core/surfaces/rate_bucket.py +21 -0
  343. core/surfaces/registry.py +51 -0
  344. core/surfaces/rendering.py +53 -0
  345. core/surfaces/send_policy.py +14 -0
  346. core/surfaces/serialize.py +24 -0
  347. core/surfaces/session_chat_registry.py +112 -0
  348. core/surfaces/session_policy.py +60 -0
  349. core/surfaces/surface.py +288 -0
  350. core/surfaces/transcription.py +88 -0
  351. core/surfaces/voice_echo.py +26 -0
  352. core/surfaces/voice_guard.py +23 -0
  353. core/tickers.py +174 -0
  354. core/tool_catalog.py +192 -0
  355. core/version.py +75 -0
  356. core/wallet/__init__.py +5 -0
  357. core/wallet/agent_wallet.py +59 -0
  358. core/wallet/audit_sink.py +61 -0
  359. core/wallet/config.py +70 -0
  360. core/wallet/factory.py +87 -0
  361. core/wallet/policy.py +104 -0
  362. core/wallet/signer.py +51 -0
  363. cron/README.md +65 -0
  364. cron/__init__.py +0 -0
  365. cron/delivery.py +277 -0
  366. cron/digest.py +124 -0
  367. cron/jobs.py +207 -0
  368. cron/runner.py +297 -0
  369. cron/schedule.py +194 -0
  370. cron/scheduler.py +201 -0
  371. cron/service.py +57 -0
  372. cron/wake_gate.py +223 -0
  373. data/__init__.py +1 -0
  374. data/prompts/autov2_prompts.json +87 -0
  375. data/prompts/skills/browser-automation/SKILL.md +43 -0
  376. data/prompts/skills/coding-workflow/SKILL.md +36 -0
  377. data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
  378. data/prompts/skills/document-writing/SKILL.md +33 -0
  379. data/prompts/skills/email-comms/SKILL.md +33 -0
  380. data/prompts/skills/file-data-ops/SKILL.md +35 -0
  381. data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
  382. data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
  383. data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
  384. data/prompts/skills/lead-research/SKILL.md +86 -0
  385. data/prompts/skills/market-research-brief/SKILL.md +104 -0
  386. data/prompts/skills/person-analyzer/SKILL.md +124 -0
  387. data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
  388. data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
  389. data/prompts/skills/polymarket-trading/SKILL.md +55 -0
  390. data/prompts/skills/presentation-creator/SKILL.md +36 -0
  391. data/prompts/skills/project-analyzer/SKILL.md +169 -0
  392. data/prompts/skills/rules.json +268 -0
  393. data/prompts/skills/secret-handling/SKILL.md +20 -0
  394. data/prompts/skills/skill-authoring/SKILL.md +31 -0
  395. data/prompts/skills/skill-security-review/SKILL.md +27 -0
  396. data/prompts/skills/social-discovery/SKILL.md +57 -0
  397. data/prompts/skills/task-planning/SKILL.md +33 -0
  398. data/prompts/skills/web-research/SKILL.md +39 -0
  399. data/prompts/skills/web-scraping/SKILL.md +73 -0
  400. data/prompts/skills/x-engagement/SKILL.md +69 -0
  401. data/prompts/system_prompts.json +4 -0
  402. data/subscription_channels.json +5 -0
  403. modules/README.md +503 -0
  404. modules/__init__.py +124 -0
  405. modules/auth/__init__.py +13 -0
  406. modules/auth/api_key_manager.py +157 -0
  407. modules/auth/identity_mapper.py +380 -0
  408. modules/auth/siwe_auth.py +273 -0
  409. modules/auth/tier_manager.py +173 -0
  410. modules/base_module.py +190 -0
  411. modules/credits/__init__.py +29 -0
  412. modules/credits/balance_manager.py +237 -0
  413. modules/credits/cost_utils.py +129 -0
  414. modules/credits/pricing.py +139 -0
  415. modules/credits/unified_ledger.py +148 -0
  416. modules/credits/usage_meter.py +310 -0
  417. modules/credits/usage_tracker.py +674 -0
  418. modules/database/__init__.py +33 -0
  419. modules/database/audit_log.py +322 -0
  420. modules/database/auth_tables.py +276 -0
  421. modules/database/connection.py +644 -0
  422. modules/database/connection_pool.py +305 -0
  423. modules/database/conversation_contexts.py +446 -0
  424. modules/database/database_manager.py +237 -0
  425. modules/database/hyperliquid.py +375 -0
  426. modules/database/polymarket.py +574 -0
  427. modules/database/user_mcp_servers.py +728 -0
  428. modules/database/user_profiles.py +562 -0
  429. modules/database/utils.py +107 -0
  430. modules/database/x402_tables.py +92 -0
  431. modules/eip8004/README.md +1069 -0
  432. modules/eip8004/__init__.py +65 -0
  433. modules/eip8004/contracts.py +498 -0
  434. modules/eip8004/models.py +271 -0
  435. modules/eip8004/registration.py +148 -0
  436. modules/eip8004/reputation.py +367 -0
  437. modules/eip8004/validation.py +277 -0
  438. modules/llm/__init__.py +97 -0
  439. modules/llm/adapters.py +963 -0
  440. modules/llm/anthropic_client.py +831 -0
  441. modules/llm/available_models.py +106 -0
  442. modules/llm/brain_scrubber.py +137 -0
  443. modules/llm/cache_hints.py +162 -0
  444. modules/llm/deepseek_client.py +562 -0
  445. modules/llm/gemini_client.py +1367 -0
  446. modules/llm/llm_client.py +1139 -0
  447. modules/llm/llm_client_registry.py +188 -0
  448. modules/llm/llm_factory.py +237 -0
  449. modules/llm/llm_manager.py +1036 -0
  450. modules/llm/messages.py +374 -0
  451. modules/llm/model_registry.py +1935 -0
  452. modules/llm/nvidia_client.py +125 -0
  453. modules/llm/openai_client.py +800 -0
  454. modules/llm/openrouter_client.py +969 -0
  455. modules/llm/profiles.py +178 -0
  456. modules/llm/test_anthropic_cache_capture.py +35 -0
  457. modules/llm/test_cached_pricing_derivation.py +24 -0
  458. modules/llm/think_scrubber.py +444 -0
  459. modules/llm/token_counter.py +268 -0
  460. modules/memory/__init__.py +35 -0
  461. modules/memory/backend_factory.py +83 -0
  462. modules/memory/cache_manager.py +186 -0
  463. modules/memory/episodic.py +108 -0
  464. modules/memory/local_vector_memory_provider.py +552 -0
  465. modules/memory/memory_manager.py +225 -0
  466. modules/memory/models.py +312 -0
  467. modules/memory/provider.py +210 -0
  468. modules/memory/registry.py +205 -0
  469. modules/memory/sqlite_memory_provider.py +767 -0
  470. modules/memory/task/__init__.py +52 -0
  471. modules/memory/task/compaction_manager.py +157 -0
  472. modules/memory/task/context_retriever.py +809 -0
  473. modules/memory/task/hierarchical_memory.py +1002 -0
  474. modules/memory/task/lexical_retriever.py +71 -0
  475. modules/memory/task/null_context_manager.py +213 -0
  476. modules/memory/task/phase_manager.py +625 -0
  477. modules/memory/task/reflection_service.py +157 -0
  478. modules/memory/task/semantic_retriever.py +218 -0
  479. modules/memory/task/task_context_manager.py +1165 -0
  480. modules/memory/task/test_forgetting_engages.py +24 -0
  481. modules/memory/task/test_reflection_llm.py +39 -0
  482. modules/memory/task/test_threat_scan.py +53 -0
  483. modules/memory/task/threat_scan.py +90 -0
  484. modules/memory/test_sqlite_memory_provider.py +106 -0
  485. modules/memory/user_profile_manager.py +527 -0
  486. modules/payments/__init__.py +11 -0
  487. modules/payments/deposit_monitor.py +478 -0
  488. modules/payments/price_oracle.py +70 -0
  489. modules/payments/treasury_sweeper.py +401 -0
  490. modules/payments/wallet_generator.py +145 -0
  491. modules/pfp/__init__.py +1 -0
  492. modules/pfp/config.py +92 -0
  493. modules/pfp/mesh.py +565 -0
  494. modules/pfp/push.py +69 -0
  495. modules/pfp/renderer.py +85 -0
  496. modules/pfp/store.py +82 -0
  497. modules/skills/__init__.py +1 -0
  498. modules/skills/skill_usage.py +248 -0
  499. modules/transcription/__init__.py +24 -0
  500. modules/transcription/base.py +20 -0
  501. modules/transcription/faster_whisper_transcriber.py +55 -0
  502. modules/x402/README.md +249 -0
  503. modules/x402/__init__.py +22 -0
  504. modules/x402/invoicing.py +416 -0
  505. modules/x402/middleware.py +364 -0
  506. modules/x402/settlement_watcher.py +145 -0
  507. modules/x402/x402_integration.py +344 -0
  508. polyrob-0.5.0.dist-info/METADATA +482 -0
  509. polyrob-0.5.0.dist-info/RECORD +745 -0
  510. polyrob-0.5.0.dist-info/WHEEL +5 -0
  511. polyrob-0.5.0.dist-info/entry_points.txt +2 -0
  512. polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
  513. polyrob-0.5.0.dist-info/top_level.txt +12 -0
  514. surfaces/README.md +70 -0
  515. surfaces/__init__.py +0 -0
  516. surfaces/email/__init__.py +0 -0
  517. surfaces/email/dedup.py +44 -0
  518. surfaces/email/harness.py +187 -0
  519. surfaces/email/inbound.py +159 -0
  520. surfaces/email/seed.py +60 -0
  521. surfaces/email/surface.py +105 -0
  522. surfaces/telegram/__init__.py +0 -0
  523. surfaces/telegram/dedup.py +83 -0
  524. surfaces/telegram/harness.py +886 -0
  525. surfaces/telegram/inbound.py +145 -0
  526. surfaces/telegram/interactive_tools.py +49 -0
  527. surfaces/telegram/markdown.py +347 -0
  528. surfaces/telegram/rate_limit.py +167 -0
  529. surfaces/telegram/surface.py +187 -0
  530. surfaces/telegram/voice.py +52 -0
  531. surfaces/whatsapp/__init__.py +0 -0
  532. surfaces/whatsapp/client.py +51 -0
  533. surfaces/whatsapp/harness.py +62 -0
  534. surfaces/whatsapp/inbound.py +132 -0
  535. surfaces/whatsapp/surface.py +64 -0
  536. surfaces/whatsapp/window.py +34 -0
  537. tools/README.md +578 -0
  538. tools/__init__.py +424 -0
  539. tools/alchemy/__init__.py +5 -0
  540. tools/alchemy/alchemy_tool.py +389 -0
  541. tools/anysite/__init__.py +17 -0
  542. tools/anysite/client.py +88 -0
  543. tools/anysite/tool.py +89 -0
  544. tools/base_tool.py +468 -0
  545. tools/browser/__init__.py +22 -0
  546. tools/browser/actions.py +118 -0
  547. tools/browser/browser.py +1457 -0
  548. tools/browser/browser_manager.py +690 -0
  549. tools/browser/context.py +1748 -0
  550. tools/browser/playwright_utils.py +137 -0
  551. tools/browser/views.py +56 -0
  552. tools/code_exec/SANDBOX_SECURITY.md +144 -0
  553. tools/code_exec/__init__.py +178 -0
  554. tools/code_exec/backend.py +73 -0
  555. tools/code_exec/backends/__init__.py +1 -0
  556. tools/code_exec/backends/docker.py +779 -0
  557. tools/code_exec/backends/local_subprocess.py +154 -0
  558. tools/code_exec/env_policy.py +45 -0
  559. tools/code_exec/result.py +42 -0
  560. tools/code_exec/sandbox_guard.py +59 -0
  561. tools/code_exec/tool.py +227 -0
  562. tools/coding/__init__.py +50 -0
  563. tools/coding/edit.py +107 -0
  564. tools/coding/search.py +113 -0
  565. tools/coding/tool.py +353 -0
  566. tools/collabland/__init__.py +5 -0
  567. tools/collabland/collabland_tool.py +471 -0
  568. tools/controller/__init__.py +0 -0
  569. tools/controller/_helpers.py +165 -0
  570. tools/controller/action_registration.py +1765 -0
  571. tools/controller/approval.py +244 -0
  572. tools/controller/approval_interactive.py +90 -0
  573. tools/controller/delegation.py +195 -0
  574. tools/controller/execution.py +797 -0
  575. tools/controller/execution_context.py +141 -0
  576. tools/controller/hooks.py +135 -0
  577. tools/controller/introspection.py +344 -0
  578. tools/controller/mcp_registrar.py +190 -0
  579. tools/controller/message_send.py +31 -0
  580. tools/controller/registry/__init__.py +4 -0
  581. tools/controller/registry/schema_generators.py +395 -0
  582. tools/controller/registry/schema_sanitizer.py +406 -0
  583. tools/controller/registry/service.py +1266 -0
  584. tools/controller/registry/views.py +151 -0
  585. tools/controller/service.py +409 -0
  586. tools/controller/tool_management.py +334 -0
  587. tools/controller/types.py +28 -0
  588. tools/controller/views.py +450 -0
  589. tools/cronjob_tools.py +166 -0
  590. tools/crypto_trade_gate.py +59 -0
  591. tools/descriptors.py +452 -0
  592. tools/dom/__init__.py +0 -0
  593. tools/dom/history_tree_processor/service.py +244 -0
  594. tools/dom/history_tree_processor/view.py +99 -0
  595. tools/dom/service.py +471 -0
  596. tools/dom/views.py +346 -0
  597. tools/email_tool.py +403 -0
  598. tools/exceptions.py +169 -0
  599. tools/filesystem.py +1038 -0
  600. tools/filesystem_docproc.py +331 -0
  601. tools/filesystem_pdf.py +755 -0
  602. tools/git/__init__.py +42 -0
  603. tools/git/tool.py +258 -0
  604. tools/github/__init__.py +33 -0
  605. tools/github/client.py +94 -0
  606. tools/github/tool.py +237 -0
  607. tools/goal_tools.py +309 -0
  608. tools/hyperliquid/__init__.py +56 -0
  609. tools/hyperliquid/models.py +233 -0
  610. tools/hyperliquid/service.py +1680 -0
  611. tools/knowledge_ingest.py +950 -0
  612. tools/mcp/README.md +535 -0
  613. tools/mcp/__init__.py +52 -0
  614. tools/mcp/catalog.py +115 -0
  615. tools/mcp/config.py +337 -0
  616. tools/mcp/mcp_tool.py +1415 -0
  617. tools/mcp/param_coercion.py +289 -0
  618. tools/mcp/protocol.py +1520 -0
  619. tools/mcp/rate_limit.py +50 -0
  620. tools/mcp/security.py +424 -0
  621. tools/mcp/self_install.py +111 -0
  622. tools/mcp/server_manager.py +829 -0
  623. tools/mcp/subscriptions.py +72 -0
  624. tools/mcp/user_mcp_service.py +899 -0
  625. tools/mcp/validation_tracker.py +89 -0
  626. tools/mcp/views.py +346 -0
  627. tools/oauth/__init__.py +17 -0
  628. tools/oauth/manager.py +75 -0
  629. tools/oauth/provider.py +69 -0
  630. tools/oauth/providers/__init__.py +1 -0
  631. tools/oauth/providers/generic_oauth2.py +96 -0
  632. tools/perplexity_tool.py +280 -0
  633. tools/polymarket/__init__.py +41 -0
  634. tools/polymarket/clob_adapter.py +76 -0
  635. tools/polymarket/models.py +182 -0
  636. tools/polymarket/service.py +2082 -0
  637. tools/self_env/__init__.py +61 -0
  638. tools/self_env/tool.py +274 -0
  639. tools/shell/__init__.py +80 -0
  640. tools/shell/backend_pool.py +118 -0
  641. tools/shell/discipline.py +62 -0
  642. tools/shell/executor.py +157 -0
  643. tools/shell/loopback_allow.py +87 -0
  644. tools/shell/process_registry.py +113 -0
  645. tools/shell/process_tool.py +131 -0
  646. tools/shell/state.py +129 -0
  647. tools/shell/tool.py +151 -0
  648. tools/task_tool.py +637 -0
  649. tools/twitter_tool.py +2086 -0
  650. tools/user_directory.py +179 -0
  651. tools/web_fetch/__init__.py +3 -0
  652. tools/web_fetch/fetcher.py +144 -0
  653. tools/web_fetch/render.py +44 -0
  654. tools/web_fetch/tool.py +60 -0
  655. tools/x402/__init__.py +60 -0
  656. tools/x402/client.py +47 -0
  657. tools/x402/invoice_tool.py +143 -0
  658. tools/x402/real_client.py +250 -0
  659. tools/x402/service.py +131 -0
  660. utils/README.md +498 -0
  661. utils/__init__.py +46 -0
  662. utils/auth_utils.py +114 -0
  663. utils/bounded_collections.py +162 -0
  664. utils/circuit_breaker.py +389 -0
  665. utils/gif_utils.py +435 -0
  666. utils/markdown_utils.py +291 -0
  667. utils/message_utils.py +147 -0
  668. utils/metrics.py +36 -0
  669. utils/path_validator.py +227 -0
  670. utils/rate_limit_manager.py +399 -0
  671. utils/result_size.py +296 -0
  672. utils/time_utils.py +206 -0
  673. utils/user_utils.py +188 -0
  674. webview/README.md +111 -0
  675. webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
  676. webview/__init__.py +8 -0
  677. webview/activity.py +636 -0
  678. webview/owner_auth.py +108 -0
  679. webview/pages.py +448 -0
  680. webview/repair_sessions.py +399 -0
  681. webview/server.py +4323 -0
  682. webview/server_launcher.py +112 -0
  683. webview/static/css/activity.css +89 -0
  684. webview/static/css/chat.css +1772 -0
  685. webview/static/css/components.css +1190 -0
  686. webview/static/css/config-panel.css +651 -0
  687. webview/static/css/pages/admin.css +766 -0
  688. webview/static/css/pages/profile.css +231 -0
  689. webview/static/css/pages/settings.css +297 -0
  690. webview/static/css/pages/signin.css +316 -0
  691. webview/static/css/style.css +4250 -0
  692. webview/static/css/variables.css +147 -0
  693. webview/static/css/workspace-fullwidth-fix.css +109 -0
  694. webview/static/img/favicon.ico +0 -0
  695. webview/static/js/activity.js +394 -0
  696. webview/static/js/admin/activity.js +186 -0
  697. webview/static/js/admin/dashboard.js +163 -0
  698. webview/static/js/admin/user_detail.js +400 -0
  699. webview/static/js/admin/users.js +174 -0
  700. webview/static/js/admin/utils.js +214 -0
  701. webview/static/js/chat.js +3178 -0
  702. webview/static/js/config-panel.js +522 -0
  703. webview/static/js/constants.js +20 -0
  704. webview/static/js/error-handler.js +226 -0
  705. webview/static/js/ethers.min.js +1 -0
  706. webview/static/js/event-filter.js +137 -0
  707. webview/static/js/event-store.js +351 -0
  708. webview/static/js/file-attachments.js +374 -0
  709. webview/static/js/file-loader.js +163 -0
  710. webview/static/js/index.js +103 -0
  711. webview/static/js/performance-utils.js +196 -0
  712. webview/static/js/profile.js +396 -0
  713. webview/static/js/screenshot.js +386 -0
  714. webview/static/js/session.js +1527 -0
  715. webview/static/js/settings.js +1288 -0
  716. webview/static/js/sidebar-data.js +766 -0
  717. webview/static/js/sidebar-toggle.js +296 -0
  718. webview/static/js/socket.io.min.js +7 -0
  719. webview/static/js/stats.js +355 -0
  720. webview/static/js/ui-utils.js +420 -0
  721. webview/static/js/workspace.js +966 -0
  722. webview/stats_service.py +613 -0
  723. webview/templates/__init__.py +1 -0
  724. webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
  725. webview/templates/activity.html +36 -0
  726. webview/templates/admin/activity.html +122 -0
  727. webview/templates/admin/dashboard.html +112 -0
  728. webview/templates/admin/user_detail.html +228 -0
  729. webview/templates/admin/users.html +105 -0
  730. webview/templates/autonomy.html +60 -0
  731. webview/templates/error.html +133 -0
  732. webview/templates/finance.html +90 -0
  733. webview/templates/identity.html +73 -0
  734. webview/templates/index.html +42 -0
  735. webview/templates/layout.html +125 -0
  736. webview/templates/memory.html +59 -0
  737. webview/templates/owner_login.html +15 -0
  738. webview/templates/profile.html +121 -0
  739. webview/templates/session.html +312 -0
  740. webview/templates/settings.html +534 -0
  741. webview/templates/sidebar.html +112 -0
  742. webview/templates/signin.html +213 -0
  743. webview/templates/status.html +14 -0
  744. webview/templates/system.html +45 -0
  745. 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">&lt;${author}&gt;</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 };