legate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,616 @@
1
+ // Legate web UI — framework JavaScript.
2
+ // Extracted from layout.slim. No server-side interpolation lives here;
3
+ // keep page-specific values in the views and call into these helpers.
4
+
5
+ // Add the CSRF token to every htmx request.
6
+ document.addEventListener('htmx:configRequest', function (e) {
7
+ var t = document.querySelector('meta[name="csrf-token"]');
8
+ if (t) e.detail.headers['X-CSRF-Token'] = t.content;
9
+ });
10
+
11
+ // Mirror the same for raw fetch(): inject the token on same-origin
12
+ // mutating requests so individual call sites don't each have to remember
13
+ // it (and can't silently 403 when they forget).
14
+ (function () {
15
+ var nativeFetch = window.fetch;
16
+ if (!nativeFetch) return;
17
+ var SAFE = { GET: true, HEAD: true, OPTIONS: true };
18
+ window.fetch = function (resource, init) {
19
+ init = init || {};
20
+ var method = (init.method || (resource && resource.method) || 'GET').toUpperCase();
21
+ var url = (typeof resource === 'string') ? resource : ((resource && resource.url) || '');
22
+ var sameOrigin = (url.charAt(0) === '/' && url.charAt(1) !== '/') ||
23
+ url === window.location.origin || url.indexOf(window.location.origin + '/') === 0;
24
+ if (!SAFE[method] && sameOrigin) {
25
+ var meta = document.querySelector('meta[name="csrf-token"]');
26
+ if (meta) {
27
+ var headers = new Headers(init.headers || (typeof resource !== 'string' && resource.headers) || undefined);
28
+ if (!headers.has('X-CSRF-Token')) headers.set('X-CSRF-Token', meta.content);
29
+ init.headers = headers;
30
+ }
31
+ }
32
+ return nativeFetch.call(this, resource, init);
33
+ };
34
+ })();
35
+
36
+ // Escape interpolated values before they go into an innerHTML sink.
37
+ window.escapeHtml = function (value) {
38
+ return String(value == null ? '' : value)
39
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
40
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
41
+ };
42
+
43
+ // Global keyboard shortcut: Cmd/Ctrl+K to focus search
44
+ document.addEventListener('keydown', function(e) {
45
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
46
+ e.preventDefault();
47
+ const searchInput = document.querySelector('#agent-search-input, .search-input, input[type="search"]');
48
+ if (searchInput) {
49
+ searchInput.focus();
50
+ searchInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
51
+ }
52
+ }
53
+ });
54
+
55
+ // Theme toggle function (global)
56
+ function toggleTheme() {
57
+ const current = document.documentElement.getAttribute('data-theme');
58
+ const next = current === 'dark' ? 'light' : 'dark';
59
+ document.documentElement.setAttribute('data-theme', next);
60
+ localStorage.setItem('legate-theme', next);
61
+ // Update icon
62
+ const icon = document.querySelector('.theme-toggle i');
63
+ if (icon) {
64
+ icon.className = next === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
65
+ }
66
+ // Update highlight.js theme
67
+ updateHljsTheme(next);
68
+ }
69
+
70
+ // Update highlight.js stylesheet based on theme
71
+ function updateHljsTheme(theme) {
72
+ const lightSheet = document.querySelector('link[data-hljs-theme="light"]');
73
+ const darkSheet = document.querySelector('link[data-hljs-theme="dark"]');
74
+ if (lightSheet && darkSheet) {
75
+ if (theme === 'dark') {
76
+ lightSheet.media = 'not all';
77
+ darkSheet.media = 'all';
78
+ } else {
79
+ lightSheet.media = 'all';
80
+ darkSheet.media = 'not all';
81
+ }
82
+ }
83
+ }
84
+
85
+ // Update theme toggle icon and highlight.js theme on load
86
+ document.addEventListener('DOMContentLoaded', () => {
87
+ const theme = document.documentElement.getAttribute('data-theme');
88
+ const icon = document.querySelector('.theme-toggle i');
89
+ if (icon) {
90
+ icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
91
+ }
92
+ // Set highlight.js theme based on saved theme preference
93
+ if (typeof updateHljsTheme === 'function') {
94
+ updateHljsTheme(theme);
95
+ }
96
+ });
97
+
98
+ document.addEventListener('DOMContentLoaded', () => {
99
+ // Navbar burger toggle
100
+ const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
101
+ if ($navbarBurgers.length > 0) {
102
+ $navbarBurgers.forEach( el => {
103
+ el.addEventListener('click', () => {
104
+ const target = el.dataset.target;
105
+ const $target = document.getElementById(target);
106
+ el.classList.toggle('is-active');
107
+ $target.classList.toggle('is-active');
108
+ });
109
+ });
110
+ }
111
+
112
+ // CodeMirror global instances and observers
113
+ window.codeMirrorInstances = window.codeMirrorInstances || {};
114
+ window.codeMirrorObservers = window.codeMirrorObservers || {};
115
+
116
+ // Initialize CodeMirror editors
117
+ function initCodeMirrorEditor(editorElement, options = {}) {
118
+ if (editorElement && editorElement.id && !window.codeMirrorInstances[editorElement.id]) {
119
+ const elementId = editorElement.id;
120
+ try {
121
+ const defaultOptions = {
122
+ lineNumbers: true,
123
+ mode: { name: "javascript", json: true },
124
+ theme: "default",
125
+ lineWrapping: true,
126
+ gutters: ["CodeMirror-lint-markers"],
127
+ lint: false, // Default to false, enable for JSON
128
+ extraKeys: {"Ctrl-Q": function(cm){ cm.foldCode(cm.getCursor()); }, "Ctrl-Space": "autocomplete"},
129
+ foldGutter: true,
130
+ gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"]
131
+ };
132
+
133
+ if (options.mode === 'text/plain' || elementId.includes('instruction')) {
134
+ options.mode = 'text/plain';
135
+ options.lint = false; // No linting for plain text
136
+ } else if (options.mode === 'markdown' || elementId.includes('some-markdown-field')) { // Example for markdown
137
+ options.mode = 'markdown';
138
+ options.lint = false;
139
+ } else { // Default to JSON mode
140
+ options.mode = { name: "javascript", json: true };
141
+ options.lint = true; // Enable linting for JSON
142
+ }
143
+
144
+ const cmOptions = Object.assign({}, defaultOptions, options);
145
+ const instance = CodeMirror.fromTextArea(editorElement, cmOptions);
146
+
147
+ instance.on('change', function(cm) {
148
+ cm.save(); // Update original textarea on change
149
+ });
150
+
151
+ window.codeMirrorInstances[elementId] = instance;
152
+ // Refresh after a slight delay to ensure layout is complete
153
+ setTimeout(() => instance.refresh(), 50);
154
+
155
+ // Observer to refresh CodeMirror when it becomes visible
156
+ const observer = new IntersectionObserver((entries) => {
157
+ entries.forEach(entry => {
158
+ if (entry.isIntersecting) {
159
+ if(window.codeMirrorInstances[elementId]) {
160
+ setTimeout(() => window.codeMirrorInstances[elementId].refresh(), 50);
161
+ }
162
+ }
163
+ });
164
+ }, { threshold: 0.1 });
165
+ observer.observe(instance.getWrapperElement());
166
+ window.codeMirrorObservers[elementId] = observer;
167
+
168
+ } catch (e) {
169
+ // Fallback if CodeMirror fails to initialize
170
+ editorElement.style.display = 'block';
171
+ console.error("CodeMirror initialization error for ID " + elementId + ":", e);
172
+ }
173
+ } else if (editorElement && editorElement.id && window.codeMirrorInstances[editorElement.id]) {
174
+ // If instance exists, just refresh it (e.g., after HTMX swap)
175
+ const elementId = editorElement.id;
176
+ setTimeout(() => {
177
+ if (window.codeMirrorInstances[elementId]) {
178
+ window.codeMirrorInstances[elementId].refresh()
179
+ }
180
+ }, 50);
181
+ }
182
+ }
183
+
184
+ // Destroy CodeMirror instance
185
+ function destroyCodeMirrorInstance(elementId) {
186
+ if (window.codeMirrorObservers && window.codeMirrorObservers[elementId]) {
187
+ window.codeMirrorObservers[elementId].disconnect();
188
+ delete window.codeMirrorObservers[elementId];
189
+ }
190
+
191
+ if (window.codeMirrorInstances && window.codeMirrorInstances[elementId]) {
192
+ const instance = window.codeMirrorInstances[elementId];
193
+ const wrapper = instance.getWrapperElement();
194
+ try {
195
+ instance.toTextArea(); // Restore original textarea
196
+ } catch (e) {
197
+ // console.error("Error converting CodeMirror to TextArea for ID " + elementId + ":", e);
198
+ }
199
+ delete window.codeMirrorInstances[elementId];
200
+ }
201
+ }
202
+
203
+ // Initialize CodeMirror for relevant textareas on the page or within a target element
204
+ function initializeRelevantEditors(targetElement = document.body) {
205
+ const editorsToInit = [
206
+ { id: 'mcp_servers_json', options: {} },
207
+ { id: 'instruction', options: { mode: 'text/plain'} },
208
+ { id: 'mcp-json-editor', options: {} },
209
+ { id: 'mcp-json-display', options: { readOnly: true } },
210
+ { id: 'task-json-editor', options: {} },
211
+ { id: 'direct-task-json-input', options: {} },
212
+ { id: 'edit-instruction-textarea', options: { mode: 'text/plain'} }
213
+ ];
214
+
215
+ editorsToInit.forEach(editor => {
216
+ let element = null;
217
+ if (targetElement && targetElement !== document.body && typeof targetElement.querySelector === 'function') {
218
+ element = targetElement.querySelector(`#${editor.id}`);
219
+ }
220
+ if (!element) {
221
+ element = document.getElementById(editor.id);
222
+ }
223
+ if (element) {
224
+ initCodeMirrorEditor(element, editor.options);
225
+ }
226
+ });
227
+ }
228
+
229
+ // Highlight.js initialization
230
+ function initializeHighlighting(targetElement = document.body) {
231
+ const selector = targetElement === document.body ? 'pre code:not(.language-mermaid):not(.mermaid)' : 'pre code:not(.language-mermaid):not(.mermaid)';
232
+ targetElement.querySelectorAll(selector).forEach(function(block) {
233
+ if (!block.classList.contains('hljs')) {
234
+ try {
235
+ hljs.highlightElement(block);
236
+ } catch (e) {
237
+ console.error("Highlight.js error:", e, "on block:", block);
238
+ }
239
+ }
240
+ });
241
+ }
242
+
243
+ // Mermaid.js preparation
244
+ function prepareMermaidContainers(targetElement = document.body) {
245
+ const selector = targetElement === document.body ? 'code.language-mermaid' : 'code.language-mermaid';
246
+ targetElement.querySelectorAll(selector).forEach(block => {
247
+ const preElement = block.closest('pre');
248
+ if (!preElement || preElement.dataset.mermaidProcessed === 'true') {
249
+ return;
250
+ }
251
+ const diagramCode = block.textContent || block.innerText;
252
+ if (!diagramCode || diagramCode.trim() === '') {
253
+ return;
254
+ }
255
+ const mermaidContainer = document.createElement('div');
256
+ mermaidContainer.className = 'mermaid';
257
+ mermaidContainer.textContent = diagramCode;
258
+ if (preElement.parentNode) {
259
+ preElement.parentNode.replaceChild(mermaidContainer, preElement);
260
+ mermaidContainer.dataset.mermaidProcessed = 'true';
261
+ }
262
+ });
263
+ }
264
+
265
+ // Initial calls on DOMContentLoaded
266
+ initializeRelevantEditors(document.body);
267
+ initializeHighlighting(document.body);
268
+ prepareMermaidContainers(document.body);
269
+ try {
270
+ const initialMermaidNodes = document.querySelectorAll('.mermaid:not(#mermaid-modal-content .mermaid)');
271
+ if (initialMermaidNodes.length > 0) {
272
+ mermaid.run({ nodes: Array.from(initialMermaidNodes) });
273
+ }
274
+ } catch (e) {
275
+ console.error("Error running mermaid.run() on initial load (non-modal):", e);
276
+ }
277
+
278
+ // Health Check Logic
279
+ if (!window.healthCheckInitialized) {
280
+ const healthCheckModal = document.getElementById('health-check-modal');
281
+ let isServerUnavailable = false;
282
+ let healthCheckIntervalId = null;
283
+
284
+ window.checkServerHealth = function() {
285
+ fetch('/healthz', { method: 'GET', cache: 'no-cache' })
286
+ .then(response => {
287
+ if (!response.ok) {
288
+ throw new Error(`Server status: ${response.status}`);
289
+ }
290
+ if (isServerUnavailable) {
291
+ // Server was down but is now back up
292
+ // Reload the page to get fresh agent statuses
293
+ // (Agent statuses are reset to 'stopped' on server restart)
294
+ if (healthCheckModal) healthCheckModal.classList.remove('is-active');
295
+ isServerUnavailable = false;
296
+ showToast("Server connection restored. Refreshing...", "is-success");
297
+ // Short delay to show the toast, then reload
298
+ setTimeout(() => {
299
+ window.location.reload();
300
+ }, 500);
301
+ }
302
+ })
303
+ .catch(error => {
304
+ if (!isServerUnavailable) {
305
+ if (healthCheckModal) healthCheckModal.classList.add('is-active');
306
+ isServerUnavailable = true;
307
+ }
308
+ });
309
+ }
310
+ if (typeof window.checkServerHealth === 'function') {
311
+ window.checkServerHealth();
312
+ healthCheckIntervalId = setInterval(window.checkServerHealth, 5000);
313
+ }
314
+ window.healthCheckInitialized = true;
315
+ }
316
+
317
+ // --- Mermaid Modal Handling Setup ---
318
+ function setupMermaidModal() {
319
+ const modal = document.getElementById('mermaid-modal');
320
+ const modalContentArea = document.getElementById('mermaid-modal-content');
321
+ const closeButtons = modal.querySelectorAll('.modal-close-button, .modal-background');
322
+
323
+ if (!modal || !modalContentArea) {
324
+ console.error("Mermaid modal main elements not found for setup.");
325
+ return;
326
+ }
327
+
328
+ function closeModal() {
329
+ modal.classList.remove('is-active');
330
+ document.documentElement.classList.remove('is-clipped');
331
+ modalContentArea.innerHTML = ''; // Clear content when modal closes
332
+ }
333
+
334
+ closeButtons.forEach(button => {
335
+ button.addEventListener('click', closeModal);
336
+ });
337
+
338
+ // Listen for clicks on the body to catch .show-mermaid-flow-btn
339
+ document.body.addEventListener('click', function(event) {
340
+ const triggerButton = event.target.closest('.show-mermaid-flow-btn');
341
+ if (triggerButton) {
342
+ event.preventDefault();
343
+ const mermaidDefinition = triggerButton.dataset.mermaidDefinition;
344
+
345
+ if (mermaidDefinition && mermaidDefinition.trim() !== '') {
346
+ modalContentArea.innerHTML = ''; // Clear previous
347
+ const mermaidTargetPre = document.createElement('pre');
348
+ mermaidTargetPre.className = 'mermaid';
349
+ mermaidTargetPre.textContent = mermaidDefinition;
350
+ modalContentArea.appendChild(mermaidTargetPre);
351
+
352
+ modal.classList.add('is-active');
353
+ document.documentElement.classList.add('is-clipped');
354
+
355
+ // Delay rendering slightly to ensure modal is visible and DOM updated
356
+ setTimeout(() => {
357
+ try {
358
+ mermaid.run({ nodes: [mermaidTargetPre] });
359
+ } catch (e) {
360
+ console.error("Error rendering Mermaid in modal:", e);
361
+ mermaidTargetPre.textContent = `Error rendering diagram:\n${e.message}\n\n${mermaidDefinition}`;
362
+ }
363
+ }, 50);
364
+ } else {
365
+ console.warn("Mermaid definition data attribute was missing or empty on button:", triggerButton);
366
+ showToast("Could not load execution flow diagram.", "is-warning");
367
+ }
368
+ }
369
+ });
370
+ }
371
+ setupMermaidModal();
372
+ // --- END Mermaid Modal Handling ---
373
+ }); // End DOMContentLoaded
374
+
375
+ // HTMX Event Listener for afterSwap
376
+ document.body.addEventListener('htmx:afterSwap', function(event) {
377
+ const targetElement = event.detail.target;
378
+ const newElement = event.detail.elt;
379
+ let elementToScan = newElement || targetElement;
380
+
381
+ if (elementToScan) {
382
+ if (typeof initializeRelevantEditors === 'function') initializeRelevantEditors(elementToScan);
383
+ if (typeof initializeHighlighting === 'function') initializeHighlighting(elementToScan);
384
+ if (typeof prepareMermaidContainers === 'function') prepareMermaidContainers(elementToScan);
385
+ try {
386
+ const mermaidNodesInSwap = elementToScan.querySelectorAll ? elementToScan.querySelectorAll('.mermaid:not(#mermaid-modal-content .mermaid)') : [];
387
+ if (mermaidNodesInSwap.length > 0) {
388
+ mermaid.run({ nodes: mermaidNodesInSwap });
389
+ }
390
+ } catch (e) {
391
+ console.error("Error running mermaid.run() after htmx swap on specific nodes:", e);
392
+ }
393
+ } else {
394
+ if (typeof initializeHighlighting === 'function') initializeHighlighting(document.body);
395
+ if (typeof prepareMermaidContainers === 'function') prepareMermaidContainers(document.body);
396
+ try {
397
+ const nonModalMermaidNodes = document.querySelectorAll('.mermaid:not(#mermaid-modal-content .mermaid)');
398
+ if (nonModalMermaidNodes.length > 0) {
399
+ mermaid.run({ nodes: Array.from(nonModalMermaidNodes) });
400
+ }
401
+ } catch (e) {
402
+ console.error("Error running mermaid.run() after htmx swap (fallback global):", e);
403
+ }
404
+ }
405
+
406
+ const triggerHeader = event.detail.xhr.getResponseHeader('HX-Trigger-After-Swap');
407
+ if (triggerHeader) {
408
+ try {
409
+ if (triggerHeader.startsWith('{') && triggerHeader.endsWith('}')) {
410
+ const triggers = JSON.parse(triggerHeader);
411
+ if (triggers.showToast && typeof showToast === 'function') {
412
+ showToast(triggers.showToast.message, triggers.showToast.type);
413
+ }
414
+ } else {
415
+ triggerHeader.split(',').forEach(triggerName => {
416
+ triggerName = triggerName.trim();
417
+ if (typeof showToast === 'function') {
418
+ if (triggerName === 'showRestartToast') {
419
+ showToast("Agent automatically restarted to apply changes.", "is-info");
420
+ } else if (triggerName === 'showRestartErrorToast') {
421
+ showToast("Error: Failed to automatically restart agent. Please stop/start manually.", "is-danger");
422
+ } else if (triggerName === 'showSaveSuccessToast') {
423
+ showToast("Configuration saved successfully.", "is-success");
424
+ }
425
+ }
426
+ });
427
+ }
428
+ } catch (e) {
429
+ if (triggerHeader === 'showRestartToast' && typeof showToast === 'function') showToast("Agent automatically restarted.", "is-info");
430
+ }
431
+ }
432
+ });
433
+
434
+ // HTMX Event Listener for beforeSwap
435
+ document.body.addEventListener('htmx:beforeSwap', function(event) {
436
+ const swapTarget = event.detail.target;
437
+ if (swapTarget && swapTarget.id === 'agent-mcp-display-container') {
438
+ const editorId = 'mcp-json-editor';
439
+ if (typeof destroyCodeMirrorInstance === 'function') destroyCodeMirrorInstance(editorId);
440
+ }
441
+ if (swapTarget && swapTarget.id === 'agent-instruction-display-container') {
442
+ const editorId = 'edit-instruction-textarea';
443
+ if (typeof destroyCodeMirrorInstance === 'function') destroyCodeMirrorInstance(editorId);
444
+ }
445
+ });
446
+
447
+ // HTMX Event Listener for afterRequest
448
+ document.body.addEventListener('htmx:afterRequest', function(event) {
449
+ const config = event.detail.requestConfig;
450
+ const xhr = event.detail.xhr;
451
+ const elt = config.elt;
452
+
453
+ if (elt && elt.id === 'generate-example-task-btn') {
454
+ const editorId = 'direct-task-json-input';
455
+ const editorInstance = window.codeMirrorInstances[editorId];
456
+ if (!editorInstance) {
457
+ if (typeof showToast === 'function') showToast("Task editor is not ready. Please wait.", "is-warning");
458
+ return;
459
+ }
460
+ if (event.detail.successful) {
461
+ try {
462
+ const responseJson = xhr.responseText;
463
+ editorInstance.setValue(responseJson);
464
+ if (typeof showToast === 'function') showToast("Example task generated.", "is-info", 2000);
465
+ } catch (e) {
466
+ if (typeof showToast === 'function') showToast("Failed to update task editor with example.", "is-danger");
467
+ }
468
+ } else {
469
+ if (typeof showToast === 'function') showToast(`Failed to generate example (Status: ${xhr.status})`, "is-danger");
470
+ }
471
+ }
472
+
473
+ const triggerHeader = xhr.getResponseHeader('HX-Trigger-After-Swap');
474
+ if (config.path && config.path.includes('/execute') && triggerHeader && triggerHeader.startsWith('showTaskError')) {
475
+ try {
476
+ const responseJson = xhr.responseText;
477
+ const parsedResponse = JSON.parse(responseJson);
478
+ const errorMessage = parsedResponse?.error || parsedResponse?.message || "An unknown execution error occurred.";
479
+ const errorHtml =
480
+ `<div class="notification is-danger is-light mt-3 is-small">
481
+ <button class="delete" type="button" onclick="this.parentElement.remove();"></button>
482
+ <strong>Execution Error:</strong><br>
483
+ <pre style="white-space: pre-wrap; word-break: break-all;">${escapeHtml(errorMessage)}</pre>
484
+ </div>`;
485
+ const targetElement = document.getElementById('task-result');
486
+ if (targetElement) {
487
+ targetElement.innerHTML = errorHtml;
488
+ }
489
+ } catch (e) {
490
+ const targetElement = document.getElementById('task-result');
491
+ if (targetElement) {
492
+ targetElement.innerHTML = '<div class="notification is-danger is-light mt-3 is-small">Failed to parse error response from server.</div>';
493
+ }
494
+ }
495
+ }
496
+ });
497
+
498
+ // Function to show toast notifications
499
+ function showToast(message, type = 'is-info', duration = 4000) {
500
+ const toastContainer = document.getElementById('toast-container');
501
+ if (!toastContainer) {
502
+ return;
503
+ }
504
+ const toast = document.createElement('div');
505
+ toast.className = `notification ${type} is-light`;
506
+ toast.style.marginBottom = '0.5em';
507
+ toast.style.opacity = '0';
508
+ toast.style.transition = 'opacity 0.3s ease-in-out';
509
+ toast.style.boxShadow = '0 2px 5px rgba(0,0,0,0.1)';
510
+ const deleteButton = document.createElement('button');
511
+ deleteButton.className = 'delete';
512
+ deleteButton.type = 'button';
513
+ deleteButton.onclick = () => {
514
+ toast.style.opacity = '0';
515
+ setTimeout(() => toast.remove(), 300);
516
+ };
517
+ toast.appendChild(deleteButton);
518
+ toast.appendChild(document.createTextNode(message));
519
+ toastContainer.appendChild(toast);
520
+ setTimeout(() => toast.style.opacity = '1', 10);
521
+ const timeoutId = setTimeout(() => {
522
+ toast.style.opacity = '0';
523
+ setTimeout(() => toast.remove(), 300);
524
+ }, duration);
525
+ deleteButton.addEventListener('click', () => clearTimeout(timeoutId));
526
+ }
527
+
528
+ // General notification closer
529
+ document.body.addEventListener('click', function(event) {
530
+ if (event.target.matches('.notification > .delete')) {
531
+ if (!event.target.closest('#toast-container')) {
532
+ event.target.parentElement.remove();
533
+ }
534
+ }
535
+ });
536
+
537
+ // Dropdown adjustment logic
538
+ function adjustDropdownDirection(dropdownId) {
539
+ const dropdown = document.getElementById(dropdownId);
540
+ if (!dropdown) return;
541
+ const menu = dropdown.querySelector('.dropdown-menu');
542
+ if (!menu) return;
543
+ const trigger = dropdown.querySelector('.dropdown-trigger button');
544
+ if (!trigger) return;
545
+ const tableContainer = trigger.closest('.table-container');
546
+ if (!tableContainer) return;
547
+
548
+ requestAnimationFrame(() => {
549
+ const menuHeight = menu.offsetHeight;
550
+ if (menuHeight === 0) {
551
+ dropdown.classList.remove('is-up');
552
+ return;
553
+ }
554
+ const triggerRect = trigger.getBoundingClientRect();
555
+ const containerRect = tableContainer.getBoundingClientRect();
556
+ const spaceBelow = containerRect.bottom - triggerRect.bottom;
557
+ if (spaceBelow < menuHeight) {
558
+ dropdown.classList.add('is-up');
559
+ } else {
560
+ dropdown.classList.remove('is-up');
561
+ }
562
+ });
563
+ }
564
+ // ---------------------------------------------------------------------------
565
+ // Shared client helpers (window.Legate)
566
+ //
567
+ // Centralizes the fetch + JSON + error + result-rendering logic that the agent
568
+ // and auth views previously each copy-pasted. The CSRF token is added by the
569
+ // global fetch() wrapper above, so callers never deal with it directly.
570
+ // ---------------------------------------------------------------------------
571
+ window.Legate = window.Legate || {};
572
+
573
+ // Same escaper exposed at window.escapeHtml; namespaced alias for new callers.
574
+ Legate.escapeHtml = window.escapeHtml;
575
+
576
+ // Perform a JSON request. Resolves with the parsed body; rejects with an Error
577
+ // whose .message is the server-provided message when available (and .status /
578
+ // .data carry the response code and payload).
579
+ Legate.apiRequest = function (url, options) {
580
+ options = options || {};
581
+ var headers = new Headers(options.headers || {});
582
+ if (!headers.has('Accept')) headers.set('Accept', 'application/json');
583
+ options.headers = headers;
584
+ return fetch(url, options).then(function (response) {
585
+ return response.json().catch(function () { return {}; }).then(function (data) {
586
+ if (!response.ok) {
587
+ var err = new Error((data && (data.error || data.message)) || ('Request failed (' + response.status + ')'));
588
+ err.status = response.status;
589
+ err.data = data;
590
+ throw err;
591
+ }
592
+ return data;
593
+ });
594
+ });
595
+ };
596
+
597
+ // Render a danger notification (escaped) into a container element.
598
+ Legate.renderError = function (el, message) {
599
+ if (!el) return;
600
+ el.innerHTML = '<div class="notification is-danger">' + Legate.escapeHtml(message) + '</div>';
601
+ };
602
+
603
+ // Render a list of { status, <labelKey>, message } items as small Bulma
604
+ // notifications. Used by the auth credential/scheme/flow result panels.
605
+ Legate.renderTestItems = function (items, opts) {
606
+ opts = opts || {};
607
+ var labelKey = opts.labelKey || 'name';
608
+ var neutralClass = opts.neutralClass || 'is-warning';
609
+ return (items || []).map(function (item) {
610
+ var statusClass = item.status === 'passed' ? 'is-success'
611
+ : item.status === 'failed' ? 'is-danger' : neutralClass;
612
+ return '<div class="notification ' + statusClass + ' is-small">' +
613
+ '<strong>' + Legate.escapeHtml(item[labelKey]) + ':</strong> ' +
614
+ Legate.escapeHtml(item.message) + '</div>';
615
+ }).join('');
616
+ };