4runr-os 2.10.85 → 2.10.86

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 (871) hide show
  1. package/README.md +2 -2
  2. package/SETUP.md +2 -2
  3. package/USER-PRIVACY.md +2 -2
  4. package/apps/os-server/DEPLOYMENT.md +426 -0
  5. package/apps/os-server/JWT-RATE-LIMIT-VALIDATION.md +462 -0
  6. package/apps/os-server/PERSISTENCE-AND-AUTH.md +227 -0
  7. package/apps/os-server/PHASE5-TESTING.md +222 -0
  8. package/apps/os-server/README.md +368 -0
  9. package/apps/os-server/TROUBLESHOOTING.md +541 -0
  10. package/apps/os-server/dist/apps/os-server/src/adapters/gateway-cancel-adapter.d.ts +3 -0
  11. package/apps/os-server/dist/apps/os-server/src/adapters/gateway-cancel-adapter.d.ts.map +1 -0
  12. package/apps/os-server/dist/apps/os-server/src/adapters/gateway-cancel-adapter.js +28 -0
  13. package/apps/os-server/dist/apps/os-server/src/adapters/gateway-cancel-adapter.js.map +1 -0
  14. package/apps/os-server/dist/apps/os-server/src/adapters/redis-sentinel-publisher.d.ts +23 -0
  15. package/apps/os-server/dist/apps/os-server/src/adapters/redis-sentinel-publisher.d.ts.map +1 -0
  16. package/apps/os-server/dist/apps/os-server/src/adapters/redis-sentinel-publisher.js +95 -0
  17. package/apps/os-server/dist/apps/os-server/src/adapters/redis-sentinel-publisher.js.map +1 -0
  18. package/apps/os-server/dist/apps/os-server/src/adapters/sentinel-event-stream.d.ts +14 -0
  19. package/apps/os-server/dist/apps/os-server/src/adapters/sentinel-event-stream.d.ts.map +1 -0
  20. package/apps/os-server/dist/apps/os-server/src/adapters/sentinel-event-stream.js +69 -0
  21. package/apps/os-server/dist/apps/os-server/src/adapters/sentinel-event-stream.js.map +1 -0
  22. package/apps/os-server/dist/apps/os-server/src/agents/definitions-simple.d.ts +10 -0
  23. package/apps/os-server/dist/apps/os-server/src/agents/definitions-simple.d.ts.map +1 -0
  24. package/apps/os-server/dist/apps/os-server/src/agents/definitions-simple.js +377 -0
  25. package/apps/os-server/dist/apps/os-server/src/agents/definitions-simple.js.map +1 -0
  26. package/apps/os-server/dist/apps/os-server/src/agents/definitions.d.ts +14 -0
  27. package/apps/os-server/dist/apps/os-server/src/agents/definitions.d.ts.map +1 -0
  28. package/apps/os-server/dist/apps/os-server/src/agents/definitions.js +210 -0
  29. package/apps/os-server/dist/apps/os-server/src/agents/definitions.js.map +1 -0
  30. package/apps/os-server/dist/apps/os-server/src/agents/local-model-provider.d.ts +25 -0
  31. package/apps/os-server/dist/apps/os-server/src/agents/local-model-provider.d.ts.map +1 -0
  32. package/apps/os-server/dist/apps/os-server/src/agents/local-model-provider.js +144 -0
  33. package/apps/os-server/dist/apps/os-server/src/agents/local-model-provider.js.map +1 -0
  34. package/apps/os-server/dist/apps/os-server/src/agents/tools.d.ts +24 -0
  35. package/apps/os-server/dist/apps/os-server/src/agents/tools.d.ts.map +1 -0
  36. package/apps/os-server/dist/apps/os-server/src/agents/tools.js +110 -0
  37. package/apps/os-server/dist/apps/os-server/src/agents/tools.js.map +1 -0
  38. package/apps/os-server/dist/apps/os-server/src/ai-providers/anthropic-provider.d.ts +14 -0
  39. package/apps/os-server/dist/apps/os-server/src/ai-providers/anthropic-provider.d.ts.map +1 -0
  40. package/apps/os-server/dist/apps/os-server/src/ai-providers/anthropic-provider.js +139 -0
  41. package/apps/os-server/dist/apps/os-server/src/ai-providers/anthropic-provider.js.map +1 -0
  42. package/apps/os-server/dist/apps/os-server/src/ai-providers/index.d.ts +6 -0
  43. package/apps/os-server/dist/apps/os-server/src/ai-providers/index.d.ts.map +1 -0
  44. package/apps/os-server/dist/apps/os-server/src/ai-providers/index.js +6 -0
  45. package/apps/os-server/dist/apps/os-server/src/ai-providers/index.js.map +1 -0
  46. package/apps/os-server/dist/apps/os-server/src/ai-providers/openai-provider.d.ts +14 -0
  47. package/apps/os-server/dist/apps/os-server/src/ai-providers/openai-provider.d.ts.map +1 -0
  48. package/apps/os-server/dist/apps/os-server/src/ai-providers/openai-provider.js +136 -0
  49. package/apps/os-server/dist/apps/os-server/src/ai-providers/openai-provider.js.map +1 -0
  50. package/apps/os-server/dist/apps/os-server/src/ai-providers/provider-manager.d.ts +18 -0
  51. package/apps/os-server/dist/apps/os-server/src/ai-providers/provider-manager.d.ts.map +1 -0
  52. package/apps/os-server/dist/apps/os-server/src/ai-providers/provider-manager.js +91 -0
  53. package/apps/os-server/dist/apps/os-server/src/ai-providers/provider-manager.js.map +1 -0
  54. package/apps/os-server/dist/apps/os-server/src/ai-providers/redis-credentials-store.d.ts +15 -0
  55. package/apps/os-server/dist/apps/os-server/src/ai-providers/redis-credentials-store.d.ts.map +1 -0
  56. package/apps/os-server/dist/apps/os-server/src/ai-providers/redis-credentials-store.js +181 -0
  57. package/apps/os-server/dist/apps/os-server/src/ai-providers/redis-credentials-store.js.map +1 -0
  58. package/apps/os-server/dist/apps/os-server/src/ai-providers/types.d.ts +54 -0
  59. package/apps/os-server/dist/apps/os-server/src/ai-providers/types.d.ts.map +1 -0
  60. package/apps/os-server/dist/apps/os-server/src/ai-providers/types.js +2 -0
  61. package/apps/os-server/dist/apps/os-server/src/ai-providers/types.js.map +1 -0
  62. package/apps/os-server/dist/apps/os-server/src/config/persistence.d.ts +5 -0
  63. package/apps/os-server/dist/apps/os-server/src/config/persistence.d.ts.map +1 -0
  64. package/apps/os-server/dist/apps/os-server/src/config/persistence.js +14 -0
  65. package/apps/os-server/dist/apps/os-server/src/config/persistence.js.map +1 -0
  66. package/apps/os-server/dist/apps/os-server/src/crypto/envelope.d.ts +28 -0
  67. package/apps/os-server/dist/apps/os-server/src/crypto/envelope.d.ts.map +1 -0
  68. package/apps/os-server/dist/apps/os-server/src/crypto/envelope.js +111 -0
  69. package/apps/os-server/dist/apps/os-server/src/crypto/envelope.js.map +1 -0
  70. package/apps/os-server/dist/apps/os-server/src/crypto/mfa-storage.d.ts +6 -0
  71. package/apps/os-server/dist/apps/os-server/src/crypto/mfa-storage.d.ts.map +1 -0
  72. package/apps/os-server/dist/apps/os-server/src/crypto/mfa-storage.js +61 -0
  73. package/apps/os-server/dist/apps/os-server/src/crypto/mfa-storage.js.map +1 -0
  74. package/apps/os-server/dist/apps/os-server/src/db/docker-manager.d.ts +21 -0
  75. package/apps/os-server/dist/apps/os-server/src/db/docker-manager.d.ts.map +1 -0
  76. package/apps/os-server/dist/apps/os-server/src/db/docker-manager.js +166 -0
  77. package/apps/os-server/dist/apps/os-server/src/db/docker-manager.js.map +1 -0
  78. package/apps/os-server/dist/apps/os-server/src/db/init.d.ts +11 -0
  79. package/apps/os-server/dist/apps/os-server/src/db/init.d.ts.map +1 -0
  80. package/apps/os-server/dist/apps/os-server/src/db/init.js +202 -0
  81. package/apps/os-server/dist/apps/os-server/src/db/init.js.map +1 -0
  82. package/apps/os-server/dist/apps/os-server/src/db/memory-db.d.ts +149 -0
  83. package/apps/os-server/dist/apps/os-server/src/db/memory-db.d.ts.map +1 -0
  84. package/apps/os-server/dist/apps/os-server/src/db/memory-db.js +295 -0
  85. package/apps/os-server/dist/apps/os-server/src/db/memory-db.js.map +1 -0
  86. package/apps/os-server/dist/apps/os-server/src/db/prisma.d.ts +4 -0
  87. package/apps/os-server/dist/apps/os-server/src/db/prisma.d.ts.map +1 -0
  88. package/apps/os-server/dist/apps/os-server/src/db/prisma.js +31 -0
  89. package/apps/os-server/dist/apps/os-server/src/db/prisma.js.map +1 -0
  90. package/apps/os-server/dist/apps/os-server/src/db/redis.d.ts +5 -0
  91. package/apps/os-server/dist/apps/os-server/src/db/redis.d.ts.map +1 -0
  92. package/apps/os-server/dist/apps/os-server/src/db/redis.js +69 -0
  93. package/apps/os-server/dist/apps/os-server/src/db/redis.js.map +1 -0
  94. package/apps/os-server/dist/apps/os-server/src/devkit/agents-api.d.ts +3 -0
  95. package/apps/os-server/dist/apps/os-server/src/devkit/agents-api.d.ts.map +1 -0
  96. package/apps/os-server/dist/apps/os-server/src/devkit/agents-api.js +369 -0
  97. package/apps/os-server/dist/apps/os-server/src/devkit/agents-api.js.map +1 -0
  98. package/apps/os-server/dist/apps/os-server/src/devkit/metrics-parser.d.ts +46 -0
  99. package/apps/os-server/dist/apps/os-server/src/devkit/metrics-parser.d.ts.map +1 -0
  100. package/apps/os-server/dist/apps/os-server/src/devkit/metrics-parser.js +114 -0
  101. package/apps/os-server/dist/apps/os-server/src/devkit/metrics-parser.js.map +1 -0
  102. package/apps/os-server/dist/apps/os-server/src/devkit/middleware.d.ts +9 -0
  103. package/apps/os-server/dist/apps/os-server/src/devkit/middleware.d.ts.map +1 -0
  104. package/apps/os-server/dist/apps/os-server/src/devkit/middleware.js +23 -0
  105. package/apps/os-server/dist/apps/os-server/src/devkit/middleware.js.map +1 -0
  106. package/apps/os-server/dist/apps/os-server/src/devkit/routes.d.ts +3 -0
  107. package/apps/os-server/dist/apps/os-server/src/devkit/routes.d.ts.map +1 -0
  108. package/apps/os-server/dist/apps/os-server/src/devkit/routes.js +270 -0
  109. package/apps/os-server/dist/apps/os-server/src/devkit/routes.js.map +1 -0
  110. package/apps/os-server/dist/apps/os-server/src/devkit/tools-api.d.ts +3 -0
  111. package/apps/os-server/dist/apps/os-server/src/devkit/tools-api.d.ts.map +1 -0
  112. package/apps/os-server/dist/apps/os-server/src/devkit/tools-api.js +183 -0
  113. package/apps/os-server/dist/apps/os-server/src/devkit/tools-api.js.map +1 -0
  114. package/apps/os-server/dist/apps/os-server/src/health/index.d.ts +36 -0
  115. package/apps/os-server/dist/apps/os-server/src/health/index.d.ts.map +1 -0
  116. package/apps/os-server/dist/apps/os-server/src/health/index.js +275 -0
  117. package/apps/os-server/dist/apps/os-server/src/health/index.js.map +1 -0
  118. package/apps/os-server/dist/apps/os-server/src/index.d.ts +2 -0
  119. package/apps/os-server/dist/apps/os-server/src/index.d.ts.map +1 -0
  120. package/apps/os-server/dist/apps/os-server/src/index.js +1131 -0
  121. package/apps/os-server/dist/apps/os-server/src/index.js.map +1 -0
  122. package/apps/os-server/dist/apps/os-server/src/metrics/index.d.ts +48 -0
  123. package/apps/os-server/dist/apps/os-server/src/metrics/index.d.ts.map +1 -0
  124. package/apps/os-server/dist/apps/os-server/src/metrics/index.js +296 -0
  125. package/apps/os-server/dist/apps/os-server/src/metrics/index.js.map +1 -0
  126. package/apps/os-server/dist/apps/os-server/src/metrics/monitoring-detail.d.ts +18 -0
  127. package/apps/os-server/dist/apps/os-server/src/metrics/monitoring-detail.d.ts.map +1 -0
  128. package/apps/os-server/dist/apps/os-server/src/metrics/monitoring-detail.js +117 -0
  129. package/apps/os-server/dist/apps/os-server/src/metrics/monitoring-detail.js.map +1 -0
  130. package/apps/os-server/dist/apps/os-server/src/middleware/auth.d.ts +5 -0
  131. package/apps/os-server/dist/apps/os-server/src/middleware/auth.d.ts.map +1 -0
  132. package/apps/os-server/dist/apps/os-server/src/middleware/auth.js +64 -0
  133. package/apps/os-server/dist/apps/os-server/src/middleware/auth.js.map +1 -0
  134. package/apps/os-server/dist/apps/os-server/src/middleware/authApiKey.d.ts +4 -0
  135. package/apps/os-server/dist/apps/os-server/src/middleware/authApiKey.d.ts.map +1 -0
  136. package/apps/os-server/dist/apps/os-server/src/middleware/authApiKey.js +105 -0
  137. package/apps/os-server/dist/apps/os-server/src/middleware/authApiKey.js.map +1 -0
  138. package/apps/os-server/dist/apps/os-server/src/middleware/authJwt.d.ts +12 -0
  139. package/apps/os-server/dist/apps/os-server/src/middleware/authJwt.d.ts.map +1 -0
  140. package/apps/os-server/dist/apps/os-server/src/middleware/authJwt.js +75 -0
  141. package/apps/os-server/dist/apps/os-server/src/middleware/authJwt.js.map +1 -0
  142. package/apps/os-server/dist/apps/os-server/src/middleware/correlationId.d.ts +6 -0
  143. package/apps/os-server/dist/apps/os-server/src/middleware/correlationId.d.ts.map +1 -0
  144. package/apps/os-server/dist/apps/os-server/src/middleware/correlationId.js +21 -0
  145. package/apps/os-server/dist/apps/os-server/src/middleware/correlationId.js.map +1 -0
  146. package/apps/os-server/dist/apps/os-server/src/middleware/ddos-protection.d.ts +13 -0
  147. package/apps/os-server/dist/apps/os-server/src/middleware/ddos-protection.d.ts.map +1 -0
  148. package/apps/os-server/dist/apps/os-server/src/middleware/ddos-protection.js +202 -0
  149. package/apps/os-server/dist/apps/os-server/src/middleware/ddos-protection.js.map +1 -0
  150. package/apps/os-server/dist/apps/os-server/src/middleware/errorHandler.d.ts +14 -0
  151. package/apps/os-server/dist/apps/os-server/src/middleware/errorHandler.d.ts.map +1 -0
  152. package/apps/os-server/dist/apps/os-server/src/middleware/errorHandler.js +98 -0
  153. package/apps/os-server/dist/apps/os-server/src/middleware/errorHandler.js.map +1 -0
  154. package/apps/os-server/dist/apps/os-server/src/middleware/log-capture.d.ts +2 -0
  155. package/apps/os-server/dist/apps/os-server/src/middleware/log-capture.d.ts.map +1 -0
  156. package/apps/os-server/dist/apps/os-server/src/middleware/log-capture.js +63 -0
  157. package/apps/os-server/dist/apps/os-server/src/middleware/log-capture.js.map +1 -0
  158. package/apps/os-server/dist/apps/os-server/src/middleware/mfa.d.ts +3 -0
  159. package/apps/os-server/dist/apps/os-server/src/middleware/mfa.d.ts.map +1 -0
  160. package/apps/os-server/dist/apps/os-server/src/middleware/mfa.js +96 -0
  161. package/apps/os-server/dist/apps/os-server/src/middleware/mfa.js.map +1 -0
  162. package/apps/os-server/dist/apps/os-server/src/middleware/rateLimit.d.ts +6 -0
  163. package/apps/os-server/dist/apps/os-server/src/middleware/rateLimit.d.ts.map +1 -0
  164. package/apps/os-server/dist/apps/os-server/src/middleware/rateLimit.js +54 -0
  165. package/apps/os-server/dist/apps/os-server/src/middleware/rateLimit.js.map +1 -0
  166. package/apps/os-server/dist/apps/os-server/src/middleware/rbac.d.ts +19 -0
  167. package/apps/os-server/dist/apps/os-server/src/middleware/rbac.d.ts.map +1 -0
  168. package/apps/os-server/dist/apps/os-server/src/middleware/rbac.js +229 -0
  169. package/apps/os-server/dist/apps/os-server/src/middleware/rbac.js.map +1 -0
  170. package/apps/os-server/dist/apps/os-server/src/middleware/security.d.ts +10 -0
  171. package/apps/os-server/dist/apps/os-server/src/middleware/security.d.ts.map +1 -0
  172. package/apps/os-server/dist/apps/os-server/src/middleware/security.js +146 -0
  173. package/apps/os-server/dist/apps/os-server/src/middleware/security.js.map +1 -0
  174. package/apps/os-server/dist/apps/os-server/src/middleware/validate.d.ts +7 -0
  175. package/apps/os-server/dist/apps/os-server/src/middleware/validate.d.ts.map +1 -0
  176. package/apps/os-server/dist/apps/os-server/src/middleware/validate.js +156 -0
  177. package/apps/os-server/dist/apps/os-server/src/middleware/validate.js.map +1 -0
  178. package/apps/os-server/dist/apps/os-server/src/queue/config.d.ts +26 -0
  179. package/apps/os-server/dist/apps/os-server/src/queue/config.d.ts.map +1 -0
  180. package/apps/os-server/dist/apps/os-server/src/queue/config.js +21 -0
  181. package/apps/os-server/dist/apps/os-server/src/queue/config.js.map +1 -0
  182. package/apps/os-server/dist/apps/os-server/src/queue/index.d.ts +13 -0
  183. package/apps/os-server/dist/apps/os-server/src/queue/index.d.ts.map +1 -0
  184. package/apps/os-server/dist/apps/os-server/src/queue/index.js +152 -0
  185. package/apps/os-server/dist/apps/os-server/src/queue/index.js.map +1 -0
  186. package/apps/os-server/dist/apps/os-server/src/queue/processor.d.ts +16 -0
  187. package/apps/os-server/dist/apps/os-server/src/queue/processor.d.ts.map +1 -0
  188. package/apps/os-server/dist/apps/os-server/src/queue/processor.js +378 -0
  189. package/apps/os-server/dist/apps/os-server/src/queue/processor.js.map +1 -0
  190. package/apps/os-server/dist/apps/os-server/src/queue/sentinel-execute-watch.d.ts +7 -0
  191. package/apps/os-server/dist/apps/os-server/src/queue/sentinel-execute-watch.d.ts.map +1 -0
  192. package/apps/os-server/dist/apps/os-server/src/queue/sentinel-execute-watch.js +22 -0
  193. package/apps/os-server/dist/apps/os-server/src/queue/sentinel-execute-watch.js.map +1 -0
  194. package/apps/os-server/dist/apps/os-server/src/routes/ai-providers-simple.d.ts +3 -0
  195. package/apps/os-server/dist/apps/os-server/src/routes/ai-providers-simple.d.ts.map +1 -0
  196. package/apps/os-server/dist/apps/os-server/src/routes/ai-providers-simple.js +175 -0
  197. package/apps/os-server/dist/apps/os-server/src/routes/ai-providers-simple.js.map +1 -0
  198. package/apps/os-server/dist/apps/os-server/src/routes/chats.d.ts +3 -0
  199. package/apps/os-server/dist/apps/os-server/src/routes/chats.d.ts.map +1 -0
  200. package/apps/os-server/dist/apps/os-server/src/routes/chats.js +150 -0
  201. package/apps/os-server/dist/apps/os-server/src/routes/chats.js.map +1 -0
  202. package/apps/os-server/dist/apps/os-server/src/routes/gdpr.d.ts +3 -0
  203. package/apps/os-server/dist/apps/os-server/src/routes/gdpr.d.ts.map +1 -0
  204. package/apps/os-server/dist/apps/os-server/src/routes/gdpr.js +252 -0
  205. package/apps/os-server/dist/apps/os-server/src/routes/gdpr.js.map +1 -0
  206. package/apps/os-server/dist/apps/os-server/src/routes/mfa.d.ts +3 -0
  207. package/apps/os-server/dist/apps/os-server/src/routes/mfa.d.ts.map +1 -0
  208. package/apps/os-server/dist/apps/os-server/src/routes/mfa.js +240 -0
  209. package/apps/os-server/dist/apps/os-server/src/routes/mfa.js.map +1 -0
  210. package/apps/os-server/dist/apps/os-server/src/routes/monitoring.d.ts +16 -0
  211. package/apps/os-server/dist/apps/os-server/src/routes/monitoring.d.ts.map +1 -0
  212. package/apps/os-server/dist/apps/os-server/src/routes/monitoring.js +168 -0
  213. package/apps/os-server/dist/apps/os-server/src/routes/monitoring.js.map +1 -0
  214. package/apps/os-server/dist/apps/os-server/src/routes/sentinel-policies.d.ts +3 -0
  215. package/apps/os-server/dist/apps/os-server/src/routes/sentinel-policies.d.ts.map +1 -0
  216. package/apps/os-server/dist/apps/os-server/src/routes/sentinel-policies.js +209 -0
  217. package/apps/os-server/dist/apps/os-server/src/routes/sentinel-policies.js.map +1 -0
  218. package/apps/os-server/dist/apps/os-server/src/routes/sentinel-predictive.d.ts +3 -0
  219. package/apps/os-server/dist/apps/os-server/src/routes/sentinel-predictive.d.ts.map +1 -0
  220. package/apps/os-server/dist/apps/os-server/src/routes/sentinel-predictive.js +88 -0
  221. package/apps/os-server/dist/apps/os-server/src/routes/sentinel-predictive.js.map +1 -0
  222. package/apps/os-server/dist/apps/os-server/src/routes/shield.d.ts +3 -0
  223. package/apps/os-server/dist/apps/os-server/src/routes/shield.d.ts.map +1 -0
  224. package/apps/os-server/dist/apps/os-server/src/routes/shield.js +85 -0
  225. package/apps/os-server/dist/apps/os-server/src/routes/shield.js.map +1 -0
  226. package/apps/os-server/dist/apps/os-server/src/routes/tool-credentials.d.ts +3 -0
  227. package/apps/os-server/dist/apps/os-server/src/routes/tool-credentials.d.ts.map +1 -0
  228. package/apps/os-server/dist/apps/os-server/src/routes/tool-credentials.js +209 -0
  229. package/apps/os-server/dist/apps/os-server/src/routes/tool-credentials.js.map +1 -0
  230. package/apps/os-server/dist/apps/os-server/src/routes/tool-proxy.d.ts +3 -0
  231. package/apps/os-server/dist/apps/os-server/src/routes/tool-proxy.d.ts.map +1 -0
  232. package/apps/os-server/dist/apps/os-server/src/routes/tool-proxy.js +206 -0
  233. package/apps/os-server/dist/apps/os-server/src/routes/tool-proxy.js.map +1 -0
  234. package/apps/os-server/dist/apps/os-server/src/runs/index.d.ts +5 -0
  235. package/apps/os-server/dist/apps/os-server/src/runs/index.d.ts.map +1 -0
  236. package/apps/os-server/dist/apps/os-server/src/runs/index.js +19 -0
  237. package/apps/os-server/dist/apps/os-server/src/runs/index.js.map +1 -0
  238. package/apps/os-server/dist/apps/os-server/src/runs/memoryRunStore.d.ts +12 -0
  239. package/apps/os-server/dist/apps/os-server/src/runs/memoryRunStore.d.ts.map +1 -0
  240. package/apps/os-server/dist/apps/os-server/src/runs/memoryRunStore.js +78 -0
  241. package/apps/os-server/dist/apps/os-server/src/runs/memoryRunStore.js.map +1 -0
  242. package/apps/os-server/dist/apps/os-server/src/runs/postgresRunStore.d.ts +15 -0
  243. package/apps/os-server/dist/apps/os-server/src/runs/postgresRunStore.d.ts.map +1 -0
  244. package/apps/os-server/dist/apps/os-server/src/runs/postgresRunStore.js +148 -0
  245. package/apps/os-server/dist/apps/os-server/src/runs/postgresRunStore.js.map +1 -0
  246. package/apps/os-server/dist/apps/os-server/src/runs/run-kill.d.ts +20 -0
  247. package/apps/os-server/dist/apps/os-server/src/runs/run-kill.d.ts.map +1 -0
  248. package/apps/os-server/dist/apps/os-server/src/runs/run-kill.js +69 -0
  249. package/apps/os-server/dist/apps/os-server/src/runs/run-kill.js.map +1 -0
  250. package/apps/os-server/dist/apps/os-server/src/runs/run-retention.d.ts +17 -0
  251. package/apps/os-server/dist/apps/os-server/src/runs/run-retention.d.ts.map +1 -0
  252. package/apps/os-server/dist/apps/os-server/src/runs/run-retention.js +124 -0
  253. package/apps/os-server/dist/apps/os-server/src/runs/run-retention.js.map +1 -0
  254. package/apps/os-server/dist/apps/os-server/src/runs/runStore.d.ts +10 -0
  255. package/apps/os-server/dist/apps/os-server/src/runs/runStore.d.ts.map +1 -0
  256. package/apps/os-server/dist/apps/os-server/src/runs/runStore.js +2 -0
  257. package/apps/os-server/dist/apps/os-server/src/runs/runStore.js.map +1 -0
  258. package/apps/os-server/dist/apps/os-server/src/runs/types.d.ts +44 -0
  259. package/apps/os-server/dist/apps/os-server/src/runs/types.d.ts.map +1 -0
  260. package/apps/os-server/dist/apps/os-server/src/runs/types.js +2 -0
  261. package/apps/os-server/dist/apps/os-server/src/runs/types.js.map +1 -0
  262. package/apps/os-server/dist/apps/os-server/src/schemas/runs.d.ts +126 -0
  263. package/apps/os-server/dist/apps/os-server/src/schemas/runs.d.ts.map +1 -0
  264. package/apps/os-server/dist/apps/os-server/src/schemas/runs.js +63 -0
  265. package/apps/os-server/dist/apps/os-server/src/schemas/runs.js.map +1 -0
  266. package/apps/os-server/dist/apps/os-server/src/security/agent-token.d.ts +51 -0
  267. package/apps/os-server/dist/apps/os-server/src/security/agent-token.d.ts.map +1 -0
  268. package/apps/os-server/dist/apps/os-server/src/security/agent-token.js +161 -0
  269. package/apps/os-server/dist/apps/os-server/src/security/agent-token.js.map +1 -0
  270. package/apps/os-server/dist/apps/os-server/src/security/outbound-url.d.ts +2 -0
  271. package/apps/os-server/dist/apps/os-server/src/security/outbound-url.d.ts.map +1 -0
  272. package/apps/os-server/dist/apps/os-server/src/security/outbound-url.js +65 -0
  273. package/apps/os-server/dist/apps/os-server/src/security/outbound-url.js.map +1 -0
  274. package/apps/os-server/dist/apps/os-server/src/security/sentinel-config-store.d.ts +14 -0
  275. package/apps/os-server/dist/apps/os-server/src/security/sentinel-config-store.d.ts.map +1 -0
  276. package/apps/os-server/dist/apps/os-server/src/security/sentinel-config-store.js +168 -0
  277. package/apps/os-server/dist/apps/os-server/src/security/sentinel-config-store.js.map +1 -0
  278. package/apps/os-server/dist/apps/os-server/src/security/sentinel-run-failure.d.ts +25 -0
  279. package/apps/os-server/dist/apps/os-server/src/security/sentinel-run-failure.d.ts.map +1 -0
  280. package/apps/os-server/dist/apps/os-server/src/security/sentinel-run-failure.js +46 -0
  281. package/apps/os-server/dist/apps/os-server/src/security/sentinel-run-failure.js.map +1 -0
  282. package/apps/os-server/dist/apps/os-server/src/security/shield-config.d.ts +14 -0
  283. package/apps/os-server/dist/apps/os-server/src/security/shield-config.d.ts.map +1 -0
  284. package/apps/os-server/dist/apps/os-server/src/security/shield-config.js +70 -0
  285. package/apps/os-server/dist/apps/os-server/src/security/shield-config.js.map +1 -0
  286. package/apps/os-server/dist/apps/os-server/src/utils/circuit-breaker.d.ts +13 -0
  287. package/apps/os-server/dist/apps/os-server/src/utils/circuit-breaker.d.ts.map +1 -0
  288. package/apps/os-server/dist/apps/os-server/src/utils/circuit-breaker.js +72 -0
  289. package/apps/os-server/dist/apps/os-server/src/utils/circuit-breaker.js.map +1 -0
  290. package/apps/os-server/dist/apps/os-server/src/utils/log-encryption.d.ts +10 -0
  291. package/apps/os-server/dist/apps/os-server/src/utils/log-encryption.d.ts.map +1 -0
  292. package/apps/os-server/dist/apps/os-server/src/utils/log-encryption.js +140 -0
  293. package/apps/os-server/dist/apps/os-server/src/utils/log-encryption.js.map +1 -0
  294. package/apps/os-server/docker-compose.local.yml +46 -0
  295. package/apps/os-server/find-gateway.sh +37 -0
  296. package/apps/os-server/jest.config.cjs +52 -0
  297. package/apps/os-server/package-lock.json +8809 -0
  298. package/apps/os-server/package.json +66 -0
  299. package/apps/os-server/scripts/setup-test-env.sh +67 -0
  300. package/apps/os-server/src/__tests__/integration/README.md +92 -0
  301. package/apps/os-server/src/__tests__/integration/helpers/test-server.ts +178 -0
  302. package/apps/os-server/src/__tests__/integration/sentinel.test.ts +364 -0
  303. package/apps/os-server/src/adapters/redis-sentinel-publisher.ts +176 -0
  304. package/apps/os-server/src/agents/definitions-simple.ts +536 -0
  305. package/apps/os-server/src/ai-providers/anthropic-provider.ts +194 -0
  306. package/apps/os-server/src/ai-providers/openai-provider.ts +193 -0
  307. package/apps/os-server/src/ai-providers/redis-credentials-store.ts +224 -0
  308. package/apps/os-server/src/db/docker-manager.ts +262 -0
  309. package/apps/os-server/src/db/init.ts +292 -0
  310. package/apps/os-server/src/devkit/routes.ts +349 -0
  311. package/apps/os-server/src/health/index.ts +375 -0
  312. package/apps/os-server/src/index.ts +1506 -0
  313. package/apps/os-server/src/middleware/correlationId.ts +56 -0
  314. package/apps/os-server/src/middleware/ddos-protection.ts +318 -0
  315. package/apps/os-server/src/middleware/log-capture.ts +92 -0
  316. package/apps/os-server/src/middleware/security.ts +222 -0
  317. package/apps/os-server/src/routes/ai-providers-simple.ts +220 -0
  318. package/apps/os-server/src/routes/gdpr.ts +337 -0
  319. package/apps/os-server/src/routes/monitoring.ts +315 -0
  320. package/apps/os-server/src/routes/sentinel-policies.ts +293 -0
  321. package/apps/os-server/src/routes/sentinel-predictive.ts +129 -0
  322. package/apps/os-server/src/routes/tool-proxy.ts +285 -0
  323. package/apps/os-server/src/security/outbound-url.ts +83 -0
  324. package/apps/os-server/update-and-test.sh +36 -0
  325. package/dist/boot-sequence.js +1 -1
  326. package/dist/boot-sequence.js.map +1 -1
  327. package/dist/gateway-api-key-bootstrap.js +1 -1
  328. package/dist/gateway-api-key-bootstrap.js.map +1 -1
  329. package/dist/gateway-prisma-bootstrap.d.ts +1 -1
  330. package/dist/gateway-prisma-bootstrap.js +1 -1
  331. package/dist/index.js +8 -8
  332. package/dist/index.js.map +1 -1
  333. package/dist/local-gateway-pid.d.ts +1 -1
  334. package/dist/local-gateway-pid.js +1 -1
  335. package/dist/os-tui-binary-bootstrap.d.ts +18 -0
  336. package/dist/os-tui-binary-bootstrap.d.ts.map +1 -0
  337. package/dist/os-tui-binary-bootstrap.js +175 -0
  338. package/dist/os-tui-binary-bootstrap.js.map +1 -0
  339. package/dist/rust-check.js +2 -2
  340. package/dist/rust-check.js.map +1 -1
  341. package/dist/system-verify.js +8 -8
  342. package/dist/system-verify.js.map +1 -1
  343. package/dist/tui-handlers.js +12 -12
  344. package/dist/tui-handlers.js.map +1 -1
  345. package/dist/watchdog.js +73 -73
  346. package/os-tui/Cargo.lock +1118 -0
  347. package/os-tui/Cargo.toml +17 -0
  348. package/os-tui/bin/os-tui.js +63 -0
  349. package/os-tui/binaries/win32-x64/os-tui.exe +0 -0
  350. package/os-tui/src/app/render_scheduler.rs +111 -0
  351. package/os-tui/src/app.rs +3814 -0
  352. package/os-tui/src/debug_log.rs +131 -0
  353. package/os-tui/src/io/mod.rs +63 -0
  354. package/os-tui/src/io/protocol.rs +14 -0
  355. package/os-tui/src/io/stdio.rs +31 -0
  356. package/os-tui/src/io/ws.rs +25 -0
  357. package/os-tui/src/main.rs +2090 -0
  358. package/os-tui/src/monitoring/mod.rs +439 -0
  359. package/os-tui/src/screens/mod.rs +254 -0
  360. package/os-tui/src/storage/cache.rs +221 -0
  361. package/os-tui/src/storage/mod.rs +5 -0
  362. package/os-tui/src/ui/agent_builder.rs +1148 -0
  363. package/os-tui/src/ui/agent_list.rs +344 -0
  364. package/os-tui/src/ui/boot.rs +145 -0
  365. package/os-tui/src/ui/connection_portal.rs +839 -0
  366. package/os-tui/src/ui/help.rs +340 -0
  367. package/os-tui/src/ui/layout.rs +978 -0
  368. package/os-tui/src/ui/mod.rs +13 -0
  369. package/os-tui/src/ui/portal_monitoring.rs +1128 -0
  370. package/os-tui/src/ui/run_manager.rs +922 -0
  371. package/os-tui/src/ui/safe_viewport.rs +236 -0
  372. package/os-tui/src/ui/sentinel_config.rs +828 -0
  373. package/os-tui/src/ui/settings.rs +414 -0
  374. package/os-tui/src/ui/setup_portal.rs +547 -0
  375. package/os-tui/src/ui/shield_dashboard.rs +1081 -0
  376. package/os-tui/src/websocket.rs +315 -0
  377. package/package.json +121 -121
  378. package/prisma/schema.prisma +470 -470
  379. package/scripts/cleanup-build-artifacts.js +2 -2
  380. package/scripts/os-tools-smoke.cjs +549 -549
  381. package/scripts/postinstall-os-server.js +63 -0
  382. package/scripts/postinstall-os-tui.js +64 -0
  383. package/scripts/seed-os-server-api-key.cjs +77 -0
  384. package/scripts/setup.js +11 -11
  385. package/src/security/package-integrity.ts +188 -188
  386. package/src/security/ssrf-protection.ts +147 -147
  387. package/apps/gateway/DEPLOYMENT.md +0 -426
  388. package/apps/gateway/JWT-RATE-LIMIT-VALIDATION.md +0 -462
  389. package/apps/gateway/PERSISTENCE-AND-AUTH.md +0 -227
  390. package/apps/gateway/PHASE5-TESTING.md +0 -222
  391. package/apps/gateway/README.md +0 -368
  392. package/apps/gateway/TROUBLESHOOTING.md +0 -541
  393. package/apps/gateway/docker-compose.local.yml +0 -46
  394. package/apps/gateway/find-gateway.sh +0 -37
  395. package/apps/gateway/jest.config.cjs +0 -52
  396. package/apps/gateway/package-lock.json +0 -8810
  397. package/apps/gateway/package.json +0 -66
  398. package/apps/gateway/scripts/setup-test-env.sh +0 -67
  399. package/apps/gateway/src/__tests__/integration/README.md +0 -92
  400. package/apps/gateway/src/__tests__/integration/helpers/test-server.ts +0 -178
  401. package/apps/gateway/src/__tests__/integration/sentinel.test.ts +0 -364
  402. package/apps/gateway/src/adapters/redis-sentinel-publisher.ts +0 -176
  403. package/apps/gateway/src/agents/definitions-simple.ts +0 -536
  404. package/apps/gateway/src/ai-providers/anthropic-provider.ts +0 -194
  405. package/apps/gateway/src/ai-providers/openai-provider.ts +0 -193
  406. package/apps/gateway/src/ai-providers/redis-credentials-store.ts +0 -224
  407. package/apps/gateway/src/db/docker-manager.ts +0 -262
  408. package/apps/gateway/src/db/init.ts +0 -292
  409. package/apps/gateway/src/devkit/routes.ts +0 -349
  410. package/apps/gateway/src/health/index.ts +0 -375
  411. package/apps/gateway/src/index.ts +0 -1506
  412. package/apps/gateway/src/middleware/correlationId.ts +0 -56
  413. package/apps/gateway/src/middleware/ddos-protection.ts +0 -318
  414. package/apps/gateway/src/middleware/log-capture.ts +0 -92
  415. package/apps/gateway/src/middleware/security.ts +0 -222
  416. package/apps/gateway/src/routes/ai-providers-simple.ts +0 -220
  417. package/apps/gateway/src/routes/gdpr.ts +0 -337
  418. package/apps/gateway/src/routes/monitoring.ts +0 -315
  419. package/apps/gateway/src/routes/sentinel-policies.ts +0 -293
  420. package/apps/gateway/src/routes/sentinel-predictive.ts +0 -129
  421. package/apps/gateway/src/routes/tool-proxy.ts +0 -285
  422. package/apps/gateway/src/security/outbound-url.ts +0 -83
  423. package/apps/gateway/update-and-test.sh +0 -36
  424. package/mk3-tui/Cargo.lock +0 -1118
  425. package/mk3-tui/Cargo.toml +0 -17
  426. package/mk3-tui/bin/mk3-tui.js +0 -63
  427. package/mk3-tui/binaries/win32-x64/mk3-tui.exe +0 -0
  428. package/mk3-tui/src/app/render_scheduler.rs +0 -111
  429. package/mk3-tui/src/app.rs +0 -3813
  430. package/mk3-tui/src/debug_log.rs +0 -131
  431. package/mk3-tui/src/io/mod.rs +0 -63
  432. package/mk3-tui/src/io/protocol.rs +0 -14
  433. package/mk3-tui/src/io/stdio.rs +0 -31
  434. package/mk3-tui/src/io/ws.rs +0 -25
  435. package/mk3-tui/src/main.rs +0 -2090
  436. package/mk3-tui/src/monitoring/mod.rs +0 -439
  437. package/mk3-tui/src/screens/mod.rs +0 -254
  438. package/mk3-tui/src/storage/cache.rs +0 -221
  439. package/mk3-tui/src/storage/mod.rs +0 -5
  440. package/mk3-tui/src/ui/agent_builder.rs +0 -1148
  441. package/mk3-tui/src/ui/agent_list.rs +0 -344
  442. package/mk3-tui/src/ui/boot.rs +0 -145
  443. package/mk3-tui/src/ui/connection_portal.rs +0 -839
  444. package/mk3-tui/src/ui/help.rs +0 -340
  445. package/mk3-tui/src/ui/layout.rs +0 -978
  446. package/mk3-tui/src/ui/mod.rs +0 -13
  447. package/mk3-tui/src/ui/portal_monitoring.rs +0 -1128
  448. package/mk3-tui/src/ui/run_manager.rs +0 -913
  449. package/mk3-tui/src/ui/safe_viewport.rs +0 -236
  450. package/mk3-tui/src/ui/sentinel_config.rs +0 -828
  451. package/mk3-tui/src/ui/settings.rs +0 -414
  452. package/mk3-tui/src/ui/setup_portal.rs +0 -547
  453. package/mk3-tui/src/ui/shield_dashboard.rs +0 -1081
  454. package/mk3-tui/src/websocket.rs +0 -315
  455. package/scripts/postinstall-gateway.js +0 -63
  456. package/scripts/postinstall-mk3.js +0 -64
  457. package/scripts/seed-gateway-api-key.cjs +0 -77
  458. /package/apps/{gateway → os-server}/.dockerignore +0 -0
  459. /package/apps/{gateway → os-server}/.eslintrc.json +0 -0
  460. /package/apps/{gateway → os-server}/Dockerfile +0 -0
  461. /package/apps/{gateway → os-server}/Dockerfile.bak +0 -0
  462. /package/apps/{gateway → os-server}/ENV_VARIABLES.md +0 -0
  463. /package/apps/{gateway → os-server}/create-test-script.sh +0 -0
  464. /package/apps/{gateway → os-server}/debug-api-responses.sh +0 -0
  465. /package/apps/{gateway → os-server}/debug-failing-tests-v2.sh +0 -0
  466. /package/apps/{gateway → os-server}/debug-failing-tests.sh +0 -0
  467. /package/apps/{gateway → os-server}/debug-responses.sh +0 -0
  468. /package/apps/{gateway → os-server}/debug-test-responses.sh +0 -0
  469. /package/apps/{gateway → os-server}/debug-tests.sh +0 -0
  470. /package/apps/{gateway → os-server}/diagnose-test-failure.sh +0 -0
  471. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/gateway-cancel-adapter.d.ts +0 -0
  472. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/gateway-cancel-adapter.d.ts.map +0 -0
  473. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/gateway-cancel-adapter.js +0 -0
  474. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/gateway-cancel-adapter.js.map +0 -0
  475. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/redis-sentinel-publisher.d.ts +0 -0
  476. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/redis-sentinel-publisher.d.ts.map +0 -0
  477. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/redis-sentinel-publisher.js +0 -0
  478. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/redis-sentinel-publisher.js.map +0 -0
  479. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/sentinel-event-stream.d.ts +0 -0
  480. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/sentinel-event-stream.d.ts.map +0 -0
  481. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/sentinel-event-stream.js +0 -0
  482. /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/sentinel-event-stream.js.map +0 -0
  483. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions-simple.d.ts +0 -0
  484. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions-simple.d.ts.map +0 -0
  485. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions-simple.js +0 -0
  486. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions-simple.js.map +0 -0
  487. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions.d.ts +0 -0
  488. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions.d.ts.map +0 -0
  489. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions.js +0 -0
  490. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions.js.map +0 -0
  491. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/local-model-provider.d.ts +0 -0
  492. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/local-model-provider.d.ts.map +0 -0
  493. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/local-model-provider.js +0 -0
  494. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/local-model-provider.js.map +0 -0
  495. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/tools.d.ts +0 -0
  496. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/tools.d.ts.map +0 -0
  497. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/tools.js +0 -0
  498. /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/tools.js.map +0 -0
  499. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/anthropic-provider.d.ts +0 -0
  500. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/anthropic-provider.d.ts.map +0 -0
  501. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/anthropic-provider.js +0 -0
  502. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/anthropic-provider.js.map +0 -0
  503. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/index.d.ts +0 -0
  504. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/index.d.ts.map +0 -0
  505. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/index.js +0 -0
  506. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/index.js.map +0 -0
  507. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/openai-provider.d.ts +0 -0
  508. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/openai-provider.d.ts.map +0 -0
  509. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/openai-provider.js +0 -0
  510. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/openai-provider.js.map +0 -0
  511. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/provider-manager.d.ts +0 -0
  512. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/provider-manager.d.ts.map +0 -0
  513. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/provider-manager.js +0 -0
  514. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/provider-manager.js.map +0 -0
  515. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/redis-credentials-store.d.ts +0 -0
  516. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/redis-credentials-store.d.ts.map +0 -0
  517. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/redis-credentials-store.js +0 -0
  518. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/redis-credentials-store.js.map +0 -0
  519. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/types.d.ts +0 -0
  520. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/types.d.ts.map +0 -0
  521. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/types.js +0 -0
  522. /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/types.js.map +0 -0
  523. /package/apps/{gateway → os-server}/dist/apps/gateway/src/config/persistence.d.ts +0 -0
  524. /package/apps/{gateway → os-server}/dist/apps/gateway/src/config/persistence.d.ts.map +0 -0
  525. /package/apps/{gateway → os-server}/dist/apps/gateway/src/config/persistence.js +0 -0
  526. /package/apps/{gateway → os-server}/dist/apps/gateway/src/config/persistence.js.map +0 -0
  527. /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/envelope.d.ts +0 -0
  528. /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/envelope.d.ts.map +0 -0
  529. /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/envelope.js +0 -0
  530. /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/envelope.js.map +0 -0
  531. /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/mfa-storage.d.ts +0 -0
  532. /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/mfa-storage.d.ts.map +0 -0
  533. /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/mfa-storage.js +0 -0
  534. /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/mfa-storage.js.map +0 -0
  535. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/docker-manager.d.ts +0 -0
  536. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/docker-manager.d.ts.map +0 -0
  537. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/docker-manager.js +0 -0
  538. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/docker-manager.js.map +0 -0
  539. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/init.d.ts +0 -0
  540. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/init.d.ts.map +0 -0
  541. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/init.js +0 -0
  542. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/init.js.map +0 -0
  543. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/memory-db.d.ts +0 -0
  544. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/memory-db.d.ts.map +0 -0
  545. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/memory-db.js +0 -0
  546. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/memory-db.js.map +0 -0
  547. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/prisma.d.ts +0 -0
  548. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/prisma.d.ts.map +0 -0
  549. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/prisma.js +0 -0
  550. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/prisma.js.map +0 -0
  551. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/redis.d.ts +0 -0
  552. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/redis.d.ts.map +0 -0
  553. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/redis.js +0 -0
  554. /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/redis.js.map +0 -0
  555. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/agents-api.d.ts +0 -0
  556. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/agents-api.d.ts.map +0 -0
  557. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/agents-api.js +0 -0
  558. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/agents-api.js.map +0 -0
  559. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/metrics-parser.d.ts +0 -0
  560. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/metrics-parser.d.ts.map +0 -0
  561. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/metrics-parser.js +0 -0
  562. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/metrics-parser.js.map +0 -0
  563. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/middleware.d.ts +0 -0
  564. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/middleware.d.ts.map +0 -0
  565. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/middleware.js +0 -0
  566. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/middleware.js.map +0 -0
  567. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/routes.d.ts +0 -0
  568. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/routes.d.ts.map +0 -0
  569. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/routes.js +0 -0
  570. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/routes.js.map +0 -0
  571. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/tools-api.d.ts +0 -0
  572. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/tools-api.d.ts.map +0 -0
  573. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/tools-api.js +0 -0
  574. /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/tools-api.js.map +0 -0
  575. /package/apps/{gateway → os-server}/dist/apps/gateway/src/health/index.d.ts +0 -0
  576. /package/apps/{gateway → os-server}/dist/apps/gateway/src/health/index.d.ts.map +0 -0
  577. /package/apps/{gateway → os-server}/dist/apps/gateway/src/health/index.js +0 -0
  578. /package/apps/{gateway → os-server}/dist/apps/gateway/src/health/index.js.map +0 -0
  579. /package/apps/{gateway → os-server}/dist/apps/gateway/src/index.d.ts +0 -0
  580. /package/apps/{gateway → os-server}/dist/apps/gateway/src/index.d.ts.map +0 -0
  581. /package/apps/{gateway → os-server}/dist/apps/gateway/src/index.js +0 -0
  582. /package/apps/{gateway → os-server}/dist/apps/gateway/src/index.js.map +0 -0
  583. /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/index.d.ts +0 -0
  584. /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/index.d.ts.map +0 -0
  585. /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/index.js +0 -0
  586. /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/index.js.map +0 -0
  587. /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +0 -0
  588. /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +0 -0
  589. /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/monitoring-detail.js +0 -0
  590. /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/monitoring-detail.js.map +0 -0
  591. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/auth.d.ts +0 -0
  592. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/auth.d.ts.map +0 -0
  593. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/auth.js +0 -0
  594. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/auth.js.map +0 -0
  595. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authApiKey.d.ts +0 -0
  596. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authApiKey.d.ts.map +0 -0
  597. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authApiKey.js +0 -0
  598. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authApiKey.js.map +0 -0
  599. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authJwt.d.ts +0 -0
  600. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authJwt.d.ts.map +0 -0
  601. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authJwt.js +0 -0
  602. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authJwt.js.map +0 -0
  603. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/correlationId.d.ts +0 -0
  604. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/correlationId.d.ts.map +0 -0
  605. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/correlationId.js +0 -0
  606. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/correlationId.js.map +0 -0
  607. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/ddos-protection.d.ts +0 -0
  608. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/ddos-protection.d.ts.map +0 -0
  609. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/ddos-protection.js +0 -0
  610. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/ddos-protection.js.map +0 -0
  611. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/errorHandler.d.ts +0 -0
  612. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/errorHandler.d.ts.map +0 -0
  613. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/errorHandler.js +0 -0
  614. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/errorHandler.js.map +0 -0
  615. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/log-capture.d.ts +0 -0
  616. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/log-capture.d.ts.map +0 -0
  617. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/log-capture.js +0 -0
  618. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/log-capture.js.map +0 -0
  619. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/mfa.d.ts +0 -0
  620. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/mfa.d.ts.map +0 -0
  621. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/mfa.js +0 -0
  622. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/mfa.js.map +0 -0
  623. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rateLimit.d.ts +0 -0
  624. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rateLimit.d.ts.map +0 -0
  625. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rateLimit.js +0 -0
  626. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rateLimit.js.map +0 -0
  627. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rbac.d.ts +0 -0
  628. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rbac.d.ts.map +0 -0
  629. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rbac.js +0 -0
  630. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rbac.js.map +0 -0
  631. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/security.d.ts +0 -0
  632. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/security.d.ts.map +0 -0
  633. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/security.js +0 -0
  634. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/security.js.map +0 -0
  635. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/validate.d.ts +0 -0
  636. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/validate.d.ts.map +0 -0
  637. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/validate.js +0 -0
  638. /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/validate.js.map +0 -0
  639. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/config.d.ts +0 -0
  640. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/config.d.ts.map +0 -0
  641. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/config.js +0 -0
  642. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/config.js.map +0 -0
  643. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/index.d.ts +0 -0
  644. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/index.d.ts.map +0 -0
  645. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/index.js +0 -0
  646. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/index.js.map +0 -0
  647. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/processor.d.ts +0 -0
  648. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/processor.d.ts.map +0 -0
  649. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/processor.js +0 -0
  650. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/processor.js.map +0 -0
  651. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/sentinel-execute-watch.d.ts +0 -0
  652. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/sentinel-execute-watch.d.ts.map +0 -0
  653. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/sentinel-execute-watch.js +0 -0
  654. /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/sentinel-execute-watch.js.map +0 -0
  655. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/ai-providers-simple.d.ts +0 -0
  656. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/ai-providers-simple.d.ts.map +0 -0
  657. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/ai-providers-simple.js +0 -0
  658. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/ai-providers-simple.js.map +0 -0
  659. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/chats.d.ts +0 -0
  660. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/chats.d.ts.map +0 -0
  661. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/chats.js +0 -0
  662. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/chats.js.map +0 -0
  663. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/gdpr.d.ts +0 -0
  664. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/gdpr.d.ts.map +0 -0
  665. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/gdpr.js +0 -0
  666. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/gdpr.js.map +0 -0
  667. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/mfa.d.ts +0 -0
  668. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/mfa.d.ts.map +0 -0
  669. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/mfa.js +0 -0
  670. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/mfa.js.map +0 -0
  671. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/monitoring.d.ts +0 -0
  672. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/monitoring.d.ts.map +0 -0
  673. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/monitoring.js +0 -0
  674. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/monitoring.js.map +0 -0
  675. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-policies.d.ts +0 -0
  676. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-policies.d.ts.map +0 -0
  677. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-policies.js +0 -0
  678. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-policies.js.map +0 -0
  679. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-predictive.d.ts +0 -0
  680. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-predictive.d.ts.map +0 -0
  681. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-predictive.js +0 -0
  682. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-predictive.js.map +0 -0
  683. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/shield.d.ts +0 -0
  684. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/shield.d.ts.map +0 -0
  685. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/shield.js +0 -0
  686. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/shield.js.map +0 -0
  687. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-credentials.d.ts +0 -0
  688. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-credentials.d.ts.map +0 -0
  689. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-credentials.js +0 -0
  690. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-credentials.js.map +0 -0
  691. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-proxy.d.ts +0 -0
  692. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-proxy.d.ts.map +0 -0
  693. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-proxy.js +0 -0
  694. /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-proxy.js.map +0 -0
  695. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/index.d.ts +0 -0
  696. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/index.d.ts.map +0 -0
  697. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/index.js +0 -0
  698. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/index.js.map +0 -0
  699. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/memoryRunStore.d.ts +0 -0
  700. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/memoryRunStore.d.ts.map +0 -0
  701. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/memoryRunStore.js +0 -0
  702. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/memoryRunStore.js.map +0 -0
  703. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/postgresRunStore.d.ts +0 -0
  704. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/postgresRunStore.d.ts.map +0 -0
  705. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/postgresRunStore.js +0 -0
  706. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/postgresRunStore.js.map +0 -0
  707. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-kill.d.ts +0 -0
  708. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-kill.d.ts.map +0 -0
  709. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-kill.js +0 -0
  710. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-kill.js.map +0 -0
  711. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-retention.d.ts +0 -0
  712. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-retention.d.ts.map +0 -0
  713. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-retention.js +0 -0
  714. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-retention.js.map +0 -0
  715. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/runStore.d.ts +0 -0
  716. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/runStore.d.ts.map +0 -0
  717. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/runStore.js +0 -0
  718. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/runStore.js.map +0 -0
  719. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/types.d.ts +0 -0
  720. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/types.d.ts.map +0 -0
  721. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/types.js +0 -0
  722. /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/types.js.map +0 -0
  723. /package/apps/{gateway → os-server}/dist/apps/gateway/src/schemas/runs.d.ts +0 -0
  724. /package/apps/{gateway → os-server}/dist/apps/gateway/src/schemas/runs.d.ts.map +0 -0
  725. /package/apps/{gateway → os-server}/dist/apps/gateway/src/schemas/runs.js +0 -0
  726. /package/apps/{gateway → os-server}/dist/apps/gateway/src/schemas/runs.js.map +0 -0
  727. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/agent-token.d.ts +0 -0
  728. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/agent-token.d.ts.map +0 -0
  729. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/agent-token.js +0 -0
  730. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/agent-token.js.map +0 -0
  731. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/outbound-url.d.ts +0 -0
  732. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/outbound-url.d.ts.map +0 -0
  733. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/outbound-url.js +0 -0
  734. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/outbound-url.js.map +0 -0
  735. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-config-store.d.ts +0 -0
  736. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-config-store.d.ts.map +0 -0
  737. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-config-store.js +0 -0
  738. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-config-store.js.map +0 -0
  739. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-run-failure.d.ts +0 -0
  740. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-run-failure.d.ts.map +0 -0
  741. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-run-failure.js +0 -0
  742. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-run-failure.js.map +0 -0
  743. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/shield-config.d.ts +0 -0
  744. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/shield-config.d.ts.map +0 -0
  745. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/shield-config.js +0 -0
  746. /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/shield-config.js.map +0 -0
  747. /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/circuit-breaker.d.ts +0 -0
  748. /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/circuit-breaker.d.ts.map +0 -0
  749. /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/circuit-breaker.js +0 -0
  750. /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/circuit-breaker.js.map +0 -0
  751. /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/log-encryption.d.ts +0 -0
  752. /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/log-encryption.d.ts.map +0 -0
  753. /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/log-encryption.js +0 -0
  754. /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/log-encryption.js.map +0 -0
  755. /package/apps/{gateway → os-server}/dist/index.js +0 -0
  756. /package/apps/{gateway → os-server}/dist/packages/adapters/redis/IdempotencyStore.d.ts +0 -0
  757. /package/apps/{gateway → os-server}/dist/packages/adapters/redis/IdempotencyStore.d.ts.map +0 -0
  758. /package/apps/{gateway → os-server}/dist/packages/adapters/redis/IdempotencyStore.js +0 -0
  759. /package/apps/{gateway → os-server}/dist/packages/adapters/redis/IdempotencyStore.js.map +0 -0
  760. /package/apps/{gateway → os-server}/dist/packages/middleware/idempotency/idempotency.d.ts +0 -0
  761. /package/apps/{gateway → os-server}/dist/packages/middleware/idempotency/idempotency.d.ts.map +0 -0
  762. /package/apps/{gateway → os-server}/dist/packages/middleware/idempotency/idempotency.js +0 -0
  763. /package/apps/{gateway → os-server}/dist/packages/middleware/idempotency/idempotency.js.map +0 -0
  764. /package/apps/{gateway → os-server}/load-tests/README.md +0 -0
  765. /package/apps/{gateway → os-server}/load-tests/k6-basic.js +0 -0
  766. /package/apps/{gateway → os-server}/load-tests/k6-rate-limit.js +0 -0
  767. /package/apps/{gateway → os-server}/minimal-test.sh +0 -0
  768. /package/apps/{gateway → os-server}/public/sentinel-dashboard.html +0 -0
  769. /package/apps/{gateway → os-server}/quick-debug.sh +0 -0
  770. /package/apps/{gateway → os-server}/scripts/seed-api-key.ts +0 -0
  771. /package/apps/{gateway → os-server}/scripts/verify-sentinel-persist.mjs +0 -0
  772. /package/apps/{gateway → os-server}/simple-test.sh +0 -0
  773. /package/apps/{gateway → os-server}/src/__tests__/agent-token.test.ts +0 -0
  774. /package/apps/{gateway → os-server}/src/__tests__/auth.test.ts +0 -0
  775. /package/apps/{gateway → os-server}/src/__tests__/correlationId.test.ts +0 -0
  776. /package/apps/{gateway → os-server}/src/__tests__/devkit-api.test.ts +0 -0
  777. /package/apps/{gateway → os-server}/src/__tests__/integration/authentication.test.ts +0 -0
  778. /package/apps/{gateway → os-server}/src/__tests__/integration/e2e-comprehensive-fixed.test.ts +0 -0
  779. /package/apps/{gateway → os-server}/src/__tests__/integration/e2e-workflow.test.ts +0 -0
  780. /package/apps/{gateway → os-server}/src/__tests__/integration/idempotency.test.ts +0 -0
  781. /package/apps/{gateway → os-server}/src/__tests__/integration/postgres-persistence.test.ts +0 -0
  782. /package/apps/{gateway → os-server}/src/__tests__/integration/rate-limiting.test.ts +0 -0
  783. /package/apps/{gateway → os-server}/src/__tests__/integration/shield.test.ts +0 -0
  784. /package/apps/{gateway → os-server}/src/__tests__/log-capture.test.ts +0 -0
  785. /package/apps/{gateway → os-server}/src/__tests__/no-persistence-mode.test.ts +0 -0
  786. /package/apps/{gateway → os-server}/src/__tests__/outbound-url.test.ts +0 -0
  787. /package/apps/{gateway → os-server}/src/__tests__/rateLimit.test.ts +0 -0
  788. /package/apps/{gateway → os-server}/src/__tests__/run-kill.test.ts +0 -0
  789. /package/apps/{gateway → os-server}/src/__tests__/run-retention.test.ts +0 -0
  790. /package/apps/{gateway → os-server}/src/__tests__/sentinel-config-persist-http.test.ts +0 -0
  791. /package/apps/{gateway → os-server}/src/__tests__/sentinel-config-store.test.ts +0 -0
  792. /package/apps/{gateway → os-server}/src/__tests__/sentinel-events.test.ts +0 -0
  793. /package/apps/{gateway → os-server}/src/__tests__/sentinel-execute-watch.test.ts +0 -0
  794. /package/apps/{gateway → os-server}/src/__tests__/sentinel-policies.test.ts +0 -0
  795. /package/apps/{gateway → os-server}/src/__tests__/sentinel-publish-kill.test.ts +0 -0
  796. /package/apps/{gateway → os-server}/src/__tests__/sentinel-run-failure.test.ts +0 -0
  797. /package/apps/{gateway → os-server}/src/__tests__/shield-config.test.ts +0 -0
  798. /package/apps/{gateway → os-server}/src/__tests__/shield-health.test.ts +0 -0
  799. /package/apps/{gateway → os-server}/src/__tests__/shield-metrics.test.ts +0 -0
  800. /package/apps/{gateway → os-server}/src/__tests__/tool-proxy.test.ts +0 -0
  801. /package/apps/{gateway → os-server}/src/__tests__/validateSecrets.test.ts +0 -0
  802. /package/apps/{gateway → os-server}/src/__tests__/validation.test.ts +0 -0
  803. /package/apps/{gateway → os-server}/src/adapters/gateway-cancel-adapter.ts +0 -0
  804. /package/apps/{gateway → os-server}/src/adapters/sentinel-event-stream.ts +0 -0
  805. /package/apps/{gateway → os-server}/src/agents/definitions.ts +0 -0
  806. /package/apps/{gateway → os-server}/src/agents/local-model-provider.ts +0 -0
  807. /package/apps/{gateway → os-server}/src/agents/tools.ts +0 -0
  808. /package/apps/{gateway → os-server}/src/ai-providers/index.ts +0 -0
  809. /package/apps/{gateway → os-server}/src/ai-providers/provider-manager.ts +0 -0
  810. /package/apps/{gateway → os-server}/src/ai-providers/types.ts +0 -0
  811. /package/apps/{gateway → os-server}/src/config/persistence.ts +0 -0
  812. /package/apps/{gateway → os-server}/src/crypto/envelope.ts +0 -0
  813. /package/apps/{gateway → os-server}/src/crypto/mfa-storage.ts +0 -0
  814. /package/apps/{gateway → os-server}/src/db/memory-db.ts +0 -0
  815. /package/apps/{gateway → os-server}/src/db/prisma.ts +0 -0
  816. /package/apps/{gateway → os-server}/src/db/redis.ts +0 -0
  817. /package/apps/{gateway → os-server}/src/devkit/agents-api.ts +0 -0
  818. /package/apps/{gateway → os-server}/src/devkit/metrics-parser.ts +0 -0
  819. /package/apps/{gateway → os-server}/src/devkit/middleware.ts +0 -0
  820. /package/apps/{gateway → os-server}/src/devkit/tools-api.ts +0 -0
  821. /package/apps/{gateway → os-server}/src/metrics/index.ts +0 -0
  822. /package/apps/{gateway → os-server}/src/metrics/monitoring-detail.ts +0 -0
  823. /package/apps/{gateway → os-server}/src/middleware/auth.ts +0 -0
  824. /package/apps/{gateway → os-server}/src/middleware/authApiKey.ts +0 -0
  825. /package/apps/{gateway → os-server}/src/middleware/authJwt.ts +0 -0
  826. /package/apps/{gateway → os-server}/src/middleware/errorHandler.ts +0 -0
  827. /package/apps/{gateway → os-server}/src/middleware/mfa.ts +0 -0
  828. /package/apps/{gateway → os-server}/src/middleware/rateLimit.ts +0 -0
  829. /package/apps/{gateway → os-server}/src/middleware/rbac.ts +0 -0
  830. /package/apps/{gateway → os-server}/src/middleware/validate.ts +0 -0
  831. /package/apps/{gateway → os-server}/src/middleware/validate.ts.backup +0 -0
  832. /package/apps/{gateway → os-server}/src/queue/config.ts +0 -0
  833. /package/apps/{gateway → os-server}/src/queue/index.ts +0 -0
  834. /package/apps/{gateway → os-server}/src/queue/processor.ts +0 -0
  835. /package/apps/{gateway → os-server}/src/queue/sentinel-execute-watch.ts +0 -0
  836. /package/apps/{gateway → os-server}/src/routes/chats.ts +0 -0
  837. /package/apps/{gateway → os-server}/src/routes/mfa.ts +0 -0
  838. /package/apps/{gateway → os-server}/src/routes/shield.ts +0 -0
  839. /package/apps/{gateway → os-server}/src/routes/tool-credentials.ts +0 -0
  840. /package/apps/{gateway → os-server}/src/runs/index.ts +0 -0
  841. /package/apps/{gateway → os-server}/src/runs/memoryRunStore.ts +0 -0
  842. /package/apps/{gateway → os-server}/src/runs/postgresRunStore.ts +0 -0
  843. /package/apps/{gateway → os-server}/src/runs/run-kill.ts +0 -0
  844. /package/apps/{gateway → os-server}/src/runs/run-retention.ts +0 -0
  845. /package/apps/{gateway → os-server}/src/runs/runStore.ts +0 -0
  846. /package/apps/{gateway → os-server}/src/runs/types.ts +0 -0
  847. /package/apps/{gateway → os-server}/src/schemas/runs.ts +0 -0
  848. /package/apps/{gateway → os-server}/src/security/agent-token.ts +0 -0
  849. /package/apps/{gateway → os-server}/src/security/sentinel-config-store.ts +0 -0
  850. /package/apps/{gateway → os-server}/src/security/sentinel-run-failure.ts +0 -0
  851. /package/apps/{gateway → os-server}/src/security/shield-config.ts +0 -0
  852. /package/apps/{gateway → os-server}/src/types/fastify-rate-limit.d.ts +0 -0
  853. /package/apps/{gateway → os-server}/src/utils/circuit-breaker.ts +0 -0
  854. /package/apps/{gateway → os-server}/src/utils/log-encryption.ts +0 -0
  855. /package/apps/{gateway → os-server}/test-all-individual.sh +0 -0
  856. /package/apps/{gateway → os-server}/test-all-phases-final.sh +0 -0
  857. /package/apps/{gateway → os-server}/test-all-phases-fixed-v2.sh +0 -0
  858. /package/apps/{gateway → os-server}/test-all-phases-fixed.sh +0 -0
  859. /package/apps/{gateway → os-server}/test-all-phases.sh +0 -0
  860. /package/apps/{gateway → os-server}/test-concurrency.sh +0 -0
  861. /package/apps/{gateway → os-server}/test-debug.sh +0 -0
  862. /package/apps/{gateway → os-server}/test-e2e.sh +0 -0
  863. /package/apps/{gateway → os-server}/test-extraction.sh +0 -0
  864. /package/apps/{gateway → os-server}/test-full-operations-final.sh +0 -0
  865. /package/apps/{gateway → os-server}/test-full-operations.sh +0 -0
  866. /package/apps/{gateway → os-server}/test-idempotency-only.sh +0 -0
  867. /package/apps/{gateway → os-server}/test-idempotency.sh +0 -0
  868. /package/apps/{gateway → os-server}/test-individual.sh +0 -0
  869. /package/apps/{gateway → os-server}/test-queue.sh +0 -0
  870. /package/apps/{gateway → os-server}/tsconfig.json +0 -0
  871. /package/{mk3-tui → os-tui}/src/ui/connection_portal.rs.backup +0 -0
@@ -0,0 +1,3814 @@
1
+ use crate::io::IoHandler;
2
+ use crate::monitoring::{MetricsDrillPanel, MonitoringSection, MonitoringState};
3
+ use crate::screens::{NavigationState, Screen};
4
+ use crate::storage::Cache;
5
+ use crate::ui::agent_builder::AgentBuilderState;
6
+ use crate::ui::run_manager::RunManagerState;
7
+ use crate::ui::sentinel_config::SentinelConfigState;
8
+ use crate::ui::shield_dashboard::ShieldDashboardState;
9
+ use crate::ui::settings::SettingsState;
10
+ use crate::websocket::WebSocketClient;
11
+ use crossterm::event::{KeyCode, KeyEvent, KeyEventKind};
12
+ use ratatui::prelude::*;
13
+ use serde::{Deserialize, Serialize};
14
+ use std::collections::{HashMap, VecDeque};
15
+ use std::time::{Duration, Instant};
16
+
17
+ /// `HH:MM:SS` for Connection Portal activity log (UTC clock; avoids pulling in `chrono`).
18
+ fn wall_clock_hms() -> String {
19
+ let secs = std::time::SystemTime::now()
20
+ .duration_since(std::time::UNIX_EPOCH)
21
+ .map(|d| d.as_secs())
22
+ .unwrap_or(0);
23
+ let h = (secs / 3600) % 24;
24
+ let m = (secs / 60) % 60;
25
+ let s = secs % 60;
26
+ format!("{h:02}:{m:02}:{s:02}")
27
+ }
28
+
29
+ mod render_scheduler;
30
+ pub use render_scheduler::{RenderScheduler, RunMode};
31
+
32
+ /// Upper bound for the operations log (`VecDeque`). Keeps TUI RAM stable under heavy traffic.
33
+ /// See `docs/REDUCING-MEMORY-AND-DISK-USAGE.md` §8.
34
+ const MAX_OPERATION_LOG_LINES: usize = 250;
35
+
36
+ #[derive(Debug, Clone, Serialize, Deserialize)]
37
+ pub enum Message {
38
+ Log { message: String, ts: String },
39
+ Posture { status: String },
40
+ Metrics { cpu: f64, mem: f64 },
41
+ Network { status: String },
42
+ Capabilities { items: Vec<String> },
43
+ }
44
+
45
+ #[derive(Debug, Clone, PartialEq)]
46
+ pub enum AppMode {
47
+ Boot,
48
+ Main,
49
+ }
50
+
51
+ #[derive(Debug, Clone, PartialEq)]
52
+ pub enum OperationMode {
53
+ Local, // No OS Server connection
54
+ Connected, // Connected to OS Server
55
+ }
56
+
57
+ impl OperationMode {
58
+ pub fn as_str(&self) -> &str {
59
+ match self {
60
+ OperationMode::Local => "LOCAL",
61
+ OperationMode::Connected => "CONNECTED",
62
+ }
63
+ }
64
+ }
65
+
66
+ #[cfg(test)]
67
+ mod portal_monitoring_tests {
68
+ use super::*;
69
+
70
+ #[test]
71
+ fn metric_history_totals_contract_keys() {
72
+ let totals = serde_json::json!({
73
+ "httpRequests": 10,
74
+ "httpRequestErrors": 1,
75
+ "queueJobsWaiting": 2,
76
+ "queueJobsActive": 3,
77
+ "queueJobsFailed": 4,
78
+ "runsActive": 5,
79
+ "sseActive": 6
80
+ });
81
+ let entry = metric_history_entry_from_totals(
82
+ "2026-01-01T00:00:00.000Z".to_string(),
83
+ totals.as_object().expect("totals object"),
84
+ );
85
+
86
+ assert_eq!(entry.http_requests, 10);
87
+ assert_eq!(entry.http_errors, 1);
88
+ assert_eq!(entry.queue_waiting, 2);
89
+ assert_eq!(entry.queue_active, 3);
90
+ assert_eq!(entry.queue_failed, 4);
91
+ assert_eq!(entry.runs_active, 5);
92
+ assert_eq!(entry.sse_active, 6);
93
+ }
94
+ }
95
+
96
+ #[derive(Debug, Clone, PartialEq)]
97
+ pub enum PortalField {
98
+ GatewayUrl,
99
+ Username,
100
+ }
101
+
102
+ #[derive(Debug, Clone, PartialEq)]
103
+ pub enum GatewayType {
104
+ FourRunrServer, // External 4Runr cloud server
105
+ LocalBundle, // Local Gateway bundle (included with Full OS)
106
+ CustomUrl, // User's own OS server
107
+ }
108
+
109
+ impl GatewayType {
110
+ pub fn as_str(&self) -> &str {
111
+ match self {
112
+ GatewayType::FourRunrServer => "4Runr Server",
113
+ GatewayType::LocalBundle => "Local Bundle",
114
+ GatewayType::CustomUrl => "Custom URL",
115
+ }
116
+ }
117
+
118
+ pub fn default_url(&self) -> &str {
119
+ match self {
120
+ GatewayType::FourRunrServer => "https://gateway.4runr.com",
121
+ GatewayType::LocalBundle => "http://localhost:3001",
122
+ GatewayType::CustomUrl => "",
123
+ }
124
+ }
125
+ }
126
+
127
+ // Activity Log for Connection Portal
128
+ #[derive(Debug, Clone)]
129
+ pub struct ActivityLogEntry {
130
+ pub timestamp: String, // HH:MM:SS format
131
+ pub level: LogLevel,
132
+ pub message: String,
133
+ }
134
+
135
+ #[derive(Debug, Clone, PartialEq)]
136
+ pub enum LogLevel {
137
+ Info,
138
+ Success,
139
+ Warning,
140
+ Error,
141
+ }
142
+
143
+ #[derive(Debug, Clone)]
144
+ pub struct ConnectionPortalState {
145
+ pub gateway_url: String,
146
+ pub username: String,
147
+ pub focused_field: PortalField,
148
+ pub connecting: bool,
149
+ /// When `connecting` became true (for elapsed timer in the portal UI).
150
+ pub connecting_started: Option<Instant>,
151
+ pub connection_success: bool,
152
+ pub error: Option<String>,
153
+ pub last_successful_url: Option<String>,
154
+ pub activity_log: Vec<ActivityLogEntry>,
155
+ }
156
+
157
+ impl Default for ConnectionPortalState {
158
+ fn default() -> Self {
159
+ Self {
160
+ gateway_url: "http://localhost:3001".to_string(),
161
+ username: String::new(),
162
+ focused_field: PortalField::GatewayUrl,
163
+ connecting: false,
164
+ connecting_started: None,
165
+ connection_success: false,
166
+ error: None,
167
+ last_successful_url: None,
168
+ activity_log: Vec::new(),
169
+ }
170
+ }
171
+ }
172
+
173
+ impl ConnectionPortalState {
174
+ pub fn reset(&mut self) {
175
+ self.gateway_url = self
176
+ .last_successful_url
177
+ .clone()
178
+ .unwrap_or_else(|| "http://localhost:3001".to_string());
179
+ self.username = String::new();
180
+ self.focused_field = PortalField::GatewayUrl;
181
+ self.connecting = false;
182
+ self.connecting_started = None;
183
+ self.connection_success = false;
184
+ self.error = None;
185
+ self.activity_log.clear();
186
+ }
187
+
188
+ pub fn begin_connecting(&mut self) {
189
+ self.connecting = true;
190
+ self.connecting_started = Some(Instant::now());
191
+ }
192
+
193
+ pub fn finish_connecting(&mut self) {
194
+ self.connecting = false;
195
+ self.connecting_started = None;
196
+ }
197
+
198
+ pub fn toggle_field(&mut self) {
199
+ self.focused_field = match self.focused_field {
200
+ PortalField::GatewayUrl => PortalField::Username,
201
+ PortalField::Username => PortalField::GatewayUrl,
202
+ };
203
+ }
204
+
205
+ pub fn add_log_entry(&mut self, timestamp: String, level: LogLevel, message: String) {
206
+ self.activity_log.push(ActivityLogEntry {
207
+ timestamp,
208
+ level,
209
+ message,
210
+ });
211
+
212
+ // Keep last N lines so the portal can show a full connect trace on small terminals
213
+ const MAX_ACTIVITY: usize = 80;
214
+ if self.activity_log.len() > MAX_ACTIVITY {
215
+ self.activity_log.remove(0);
216
+ }
217
+ }
218
+ }
219
+
220
+ /// Gateway traffic snapshot (Prometheus `/metrics` via CLI WebSocket handler).
221
+ #[derive(Debug, Clone, Serialize)]
222
+ pub struct MetricHistoryEntry {
223
+ pub timestamp: String,
224
+ pub http_requests: u64,
225
+ pub http_errors: u64,
226
+ pub queue_waiting: u64,
227
+ pub queue_active: u64,
228
+ pub queue_failed: u64,
229
+ pub runs_active: u64,
230
+ pub sse_active: u64,
231
+ }
232
+
233
+ /// Phase 5 contract with `packages/os-cli/src/gateway-observability.ts` `totals`.
234
+ /// If CLI keys change, update this mapping and `metric_history_totals_contract_keys`.
235
+ pub fn metric_history_entry_from_totals(
236
+ timestamp: String,
237
+ totals: &serde_json::Map<String, serde_json::Value>,
238
+ ) -> MetricHistoryEntry {
239
+ fn read_u64(totals: &serde_json::Map<String, serde_json::Value>, key: &str) -> u64 {
240
+ totals
241
+ .get(key)
242
+ .and_then(|v| v.as_u64().or_else(|| v.as_f64().map(|f| f.max(0.0) as u64)))
243
+ .unwrap_or(0)
244
+ }
245
+
246
+ MetricHistoryEntry {
247
+ timestamp,
248
+ http_requests: read_u64(totals, "httpRequests"),
249
+ http_errors: read_u64(totals, "httpRequestErrors"),
250
+ queue_waiting: read_u64(totals, "queueJobsWaiting"),
251
+ queue_active: read_u64(totals, "queueJobsActive"),
252
+ queue_failed: read_u64(totals, "queueJobsFailed"),
253
+ runs_active: read_u64(totals, "runsActive"),
254
+ sse_active: read_u64(totals, "sseActive"),
255
+ }
256
+ }
257
+
258
+ #[derive(Debug, Clone, Serialize)]
259
+ pub struct DependencyAlertEntry {
260
+ pub timestamp: String,
261
+ pub previous: String,
262
+ pub current: String,
263
+ }
264
+
265
+ #[derive(Debug, Clone)]
266
+ pub struct PortalMonitoringState {
267
+ pub loading: bool,
268
+ pub error: Option<String>,
269
+ pub last_updated: Option<String>,
270
+ pub content_lines: Vec<String>,
271
+ pub scroll_offset: usize,
272
+ /// Width-based clip for section summaries; updated each render (Phase 1).
273
+ pub summary_clip_width: usize,
274
+ /// Body inner height from last render (for scroll bounds).
275
+ pub viewport_lines: usize,
276
+ /// Auto-refresh timer
277
+ pub last_refresh: Option<Instant>,
278
+ pub auto_refresh_interval: Duration,
279
+ pub auto_refresh_enabled: bool,
280
+ /// Previous HTTP total (from `totals.httpRequests`) for a per-refresh "live" delta line.
281
+ pub last_http_for_delta: Option<u64>,
282
+ /// Wall clock at last successful observability poll (for rate estimate).
283
+ pub last_instant_for_delta: Option<Instant>,
284
+ /// Phase 2: per-section lines from `monitoring.refresh` / `monitoring.logs` (merged over snapshot).
285
+ pub section_overrides: HashMap<MonitoringSection, Vec<String>>,
286
+ /// Which section is waiting on `monitoring.refresh`.
287
+ pub section_refresh_loading: Option<MonitoringSection>,
288
+ pub logs_fetch_loading: bool,
289
+ /// Phase 3: drill-down inside Metrics (`→` / `←`); which prom-client slice is shown.
290
+ pub metrics_drill: Option<MetricsDrillPanel>,
291
+ pub metrics_drill_lines: Vec<String>,
292
+ pub metrics_drill_loading: bool,
293
+ /// Last N HTTP request totals from full snapshots (for ASCII sparkline).
294
+ pub http_total_sparkline: VecDeque<u64>,
295
+ /// Phase 5: bounded sample history (720 samples; wall-clock span depends on refresh cadence).
296
+ pub metric_history: VecDeque<MetricHistoryEntry>,
297
+ pub dependency_alerts: VecDeque<DependencyAlertEntry>,
298
+ pub last_dependency_status: Option<String>,
299
+ pub trend_view: bool,
300
+ pub last_export_path: Option<String>,
301
+ pub help_visible: bool,
302
+ }
303
+
304
+ impl Default for PortalMonitoringState {
305
+ fn default() -> Self {
306
+ Self {
307
+ loading: false,
308
+ error: None,
309
+ last_updated: None,
310
+ content_lines: Vec::new(),
311
+ scroll_offset: 0,
312
+ summary_clip_width: 80,
313
+ viewport_lines: 18,
314
+ last_refresh: None,
315
+ auto_refresh_interval: Duration::from_secs(5),
316
+ auto_refresh_enabled: false,
317
+ last_http_for_delta: None,
318
+ last_instant_for_delta: None,
319
+ section_overrides: HashMap::new(),
320
+ section_refresh_loading: None,
321
+ logs_fetch_loading: false,
322
+ metrics_drill: None,
323
+ metrics_drill_lines: Vec::new(),
324
+ metrics_drill_loading: false,
325
+ http_total_sparkline: VecDeque::new(),
326
+ metric_history: VecDeque::new(),
327
+ dependency_alerts: VecDeque::new(),
328
+ last_dependency_status: None,
329
+ trend_view: false,
330
+ last_export_path: None,
331
+ help_visible: false,
332
+ }
333
+ }
334
+ }
335
+
336
+ /// Section-based monitoring UI state (Phase 1).
337
+ #[derive(Debug, Clone)]
338
+ pub struct AdvancedMonitoringState {
339
+ pub monitoring_state: MonitoringState,
340
+ /// Optional mirror of the connected OS Server URL at construction time. **Live URL for UI is always
341
+ /// `AppState.gateway_url`** (and `MonitoringState::gateway_url` is synced from there in Portal Monitoring render).
342
+ #[allow(dead_code)]
343
+ pub gateway_url: Option<String>,
344
+ }
345
+
346
+ impl Default for AdvancedMonitoringState {
347
+ fn default() -> Self {
348
+ Self {
349
+ monitoring_state: MonitoringState::default(),
350
+ gateway_url: None,
351
+ }
352
+ }
353
+ }
354
+
355
+ impl AdvancedMonitoringState {
356
+ pub fn new(gateway_url: Option<String>) -> Self {
357
+ Self {
358
+ monitoring_state: MonitoringState::new(gateway_url.clone()),
359
+ gateway_url,
360
+ }
361
+ }
362
+ }
363
+
364
+ #[derive(Debug, Clone)]
365
+ pub struct AppState {
366
+ // Navigation state (NEW - replaces simple mode)
367
+ pub navigation: NavigationState,
368
+
369
+ // App mode (DEPRECATED - keeping for backward compatibility during transition)
370
+ pub mode: AppMode,
371
+
372
+ // Boot state
373
+ pub boot_progress: u16, // 0-100
374
+ pub boot_lines: VecDeque<String>,
375
+ pub boot_done: bool,
376
+ pub boot_started_at: Instant,
377
+ // Connection state
378
+ pub connected: bool,
379
+ #[allow(dead_code)]
380
+ pub gateway_url: Option<String>,
381
+
382
+ // Real component status
383
+ #[allow(dead_code)]
384
+ pub gateway_healthy: bool,
385
+ /// Last `gateway.health` hint for 4Runr DB (from `/health` → `fourRunrDb`).
386
+ pub gateway_db_summary: Option<String>,
387
+ pub posture_status: String,
388
+ pub shield_mode: String, // "off", "monitor", "enforce"
389
+ pub shield_detectors: Vec<String>, // ["pii", "injection", "hallucination"]
390
+ pub shield_blocks_total: u64,
391
+ pub shield_masks_total: u64,
392
+ pub sentinel_state: String, // "idle", "watching", "triggered"
393
+ pub sentinel_active_runs: usize,
394
+
395
+ // Real metrics
396
+ pub total_runs: u64,
397
+ #[allow(dead_code)]
398
+ pub active_sse_connections: u64,
399
+ #[allow(dead_code)]
400
+ pub idempotency_store_size: u64,
401
+
402
+ // System resources
403
+ pub cpu: f64,
404
+ pub mem: f64,
405
+ pub network_status: String,
406
+
407
+ // Real logs
408
+ pub logs: VecDeque<String>,
409
+
410
+ // Capabilities
411
+ pub capabilities: Vec<String>,
412
+
413
+ // UI state
414
+ pub command_input: String,
415
+ pub command_focused: bool,
416
+ pub log_scroll: usize,
417
+
418
+ // Animation state
419
+ pub tick: u64,
420
+ pub spinner_frame: usize,
421
+ pub uptime_secs: u64, // Updated from real clock in main loop, not from tick()
422
+
423
+ // Perf overlay
424
+ pub perf_overlay: bool,
425
+ pub render_count: u64,
426
+ pub last_render_time: std::time::Instant,
427
+ pub render_durations: VecDeque<u64>, // ms
428
+ pub render_scheduled_count: u64,
429
+ pub log_write_count: u64,
430
+
431
+ // Screen-specific state
432
+ pub agent_builder: AgentBuilderState,
433
+ pub run_manager: RunManagerState,
434
+ pub sentinel_config: SentinelConfigState,
435
+ pub shield_dashboard: ShieldDashboardState,
436
+ pub settings: SettingsState,
437
+ pub agent_list: AgentListState,
438
+ pub connection_portal: ConnectionPortalState,
439
+ pub setup_portal: SetupPortalState,
440
+ pub portal_monitoring: PortalMonitoringState,
441
+ /// Last successful observability pull (for auto-refresh interval).
442
+ pub portal_observability_last_poll: Option<Instant>,
443
+
444
+ /// Section selection / expand state for Portal Monitoring (Phase 1).
445
+ pub advanced_monitoring: AdvancedMonitoringState,
446
+
447
+ // Command tracking for response handling (Step 5.1)
448
+ pub pending_agent_create_id: Option<String>,
449
+ pub pending_agent_list_id: Option<String>,
450
+ pub pending_agent_delete_id: Option<String>,
451
+ pub pending_gateway_connect_id: Option<String>,
452
+ pub pending_gateway_health_id: Option<String>, // Phase 2.1: Track health checks
453
+ pub pending_gateway_observability_id: Option<String>,
454
+ pub pending_monitoring_refresh_id: Option<String>,
455
+ pub pending_monitoring_logs_id: Option<String>,
456
+ pub pending_monitoring_drill_id: Option<String>,
457
+ pub pending_setup_detect_id: Option<String>, // Track setup.detect command
458
+
459
+ // Step 6: Gateway runs
460
+ pub pending_run_list_id: Option<String>,
461
+ pub pending_run_list_silent: bool,
462
+ pub pending_run_get_id: Option<String>,
463
+ pub pending_run_cancel_id: Option<String>,
464
+ pub pending_run_quick_id: Option<String>,
465
+
466
+ pub pending_sentinel_load_id: Option<String>,
467
+ pub pending_sentinel_apply_id: Option<String>,
468
+ pub pending_shield_load_id: Option<String>,
469
+
470
+ // Deletion confirmation (Step 5.5)
471
+ pub agent_delete_requested: bool,
472
+
473
+ // Mode tracking (Step 7)
474
+ pub operation_mode: OperationMode,
475
+
476
+ // Local cache
477
+ pub cache: Option<Cache>,
478
+ pub cache_loaded: bool,
479
+ }
480
+
481
+ impl Default for AppState {
482
+ fn default() -> Self {
483
+ Self {
484
+ navigation: NavigationState::new(),
485
+ mode: AppMode::Boot,
486
+ boot_progress: 0,
487
+ boot_lines: VecDeque::new(),
488
+ boot_done: false,
489
+ boot_started_at: Instant::now(),
490
+ connected: false,
491
+ gateway_url: None,
492
+ gateway_healthy: false,
493
+ gateway_db_summary: None,
494
+ posture_status: "Demo Mode".to_string(),
495
+ shield_mode: "enforce".to_string(),
496
+ shield_detectors: vec!["pii".into(), "injection".into(), "hallucination".into()],
497
+ shield_blocks_total: 0,
498
+ shield_masks_total: 0,
499
+ sentinel_state: "idle".to_string(),
500
+ sentinel_active_runs: 0,
501
+ total_runs: 0,
502
+ active_sse_connections: 0,
503
+ idempotency_store_size: 0,
504
+ cpu: 0.15,
505
+ mem: 0.25,
506
+ network_status: "Not Connected".to_string(),
507
+ logs: VecDeque::from([
508
+ "[SYSTEM] 4Runr AI Agent OS initialized".to_string(),
509
+ "[MODE] Starting in LOCAL mode (offline)".to_string(),
510
+ "[SHIELD] Safety layer active (enforce mode)".to_string(),
511
+ "[SHIELD] Detectors: PII, Injection, Hallucination".to_string(),
512
+ "[SENTINEL] Real-time monitoring ready".to_string(),
513
+ "[HELP] Type 'connect portal' to connect to OS server".to_string(),
514
+ "[HELP] Type 'help' for available commands".to_string(),
515
+ ]),
516
+ capabilities: vec![
517
+ "text-analysis".to_string(),
518
+ "code-review".to_string(),
519
+ "data-extraction".to_string(),
520
+ ],
521
+ command_input: String::new(),
522
+ command_focused: false,
523
+ log_scroll: 0,
524
+ tick: 0,
525
+ spinner_frame: 0,
526
+ uptime_secs: 0,
527
+ perf_overlay: false,
528
+ render_count: 0,
529
+ last_render_time: std::time::Instant::now(),
530
+ render_durations: VecDeque::with_capacity(60),
531
+ render_scheduled_count: 0,
532
+ log_write_count: 0,
533
+ agent_builder: AgentBuilderState::default(),
534
+ run_manager: RunManagerState::default(),
535
+ sentinel_config: SentinelConfigState::default(),
536
+ shield_dashboard: ShieldDashboardState::default(),
537
+ settings: SettingsState::default(),
538
+ agent_list: AgentListState::default(),
539
+ connection_portal: ConnectionPortalState::default(),
540
+ setup_portal: SetupPortalState::default(),
541
+ portal_monitoring: PortalMonitoringState::default(),
542
+ portal_observability_last_poll: None,
543
+ advanced_monitoring: AdvancedMonitoringState::default(),
544
+ pending_agent_create_id: None,
545
+ pending_agent_list_id: None,
546
+ pending_agent_delete_id: None,
547
+ pending_gateway_connect_id: None,
548
+ pending_gateway_health_id: None, // Phase 2.1: Initialize health check tracking
549
+ pending_gateway_observability_id: None,
550
+ pending_monitoring_refresh_id: None,
551
+ pending_monitoring_logs_id: None,
552
+ pending_monitoring_drill_id: None,
553
+ pending_setup_detect_id: None,
554
+ pending_run_list_id: None,
555
+ pending_run_list_silent: false,
556
+ pending_run_get_id: None,
557
+ pending_run_cancel_id: None,
558
+ pending_run_quick_id: None,
559
+ pending_sentinel_load_id: None,
560
+ pending_sentinel_apply_id: None,
561
+ pending_shield_load_id: None,
562
+ agent_delete_requested: false,
563
+ operation_mode: OperationMode::Local,
564
+ cache: Cache::new().ok(),
565
+ cache_loaded: false,
566
+ }
567
+ }
568
+ }
569
+
570
+ #[derive(Debug, Clone, PartialEq)]
571
+ pub enum GatewayOption {
572
+ LocalBundle,
573
+ CloudServer,
574
+ CustomUrl,
575
+ }
576
+
577
+ impl GatewayOption {
578
+ pub fn as_str(&self) -> &str {
579
+ match self {
580
+ GatewayOption::LocalBundle => "Local Bundle",
581
+ GatewayOption::CloudServer => "4Runr Server",
582
+ GatewayOption::CustomUrl => "Custom URL",
583
+ }
584
+ }
585
+
586
+ pub fn default_url(&self) -> &str {
587
+ match self {
588
+ GatewayOption::LocalBundle => "http://localhost:3001",
589
+ GatewayOption::CloudServer => "https://api.4runr.com",
590
+ GatewayOption::CustomUrl => "",
591
+ }
592
+ }
593
+ }
594
+
595
+ #[derive(Debug, Clone)]
596
+ pub struct SetupPortalState {
597
+ pub selected_option: GatewayOption,
598
+ pub detecting: bool,
599
+ /// When detection started (for timeout)
600
+ pub detecting_since: Option<std::time::Instant>,
601
+ pub detection_result: Option<DetectionResult>,
602
+ pub error: Option<String>,
603
+ }
604
+
605
+ #[derive(Debug, Clone)]
606
+ pub struct DetectionResult {
607
+ pub local_bundle: BundleStatus,
608
+ pub cloud_server: CloudStatus,
609
+ }
610
+
611
+ #[derive(Debug, Clone)]
612
+ pub struct BundleStatus {
613
+ pub available: bool,
614
+ pub running: bool,
615
+ pub path: Option<String>,
616
+ }
617
+
618
+ #[derive(Debug, Clone)]
619
+ pub struct CloudStatus {
620
+ pub available: bool,
621
+ pub status: String,
622
+ }
623
+
624
+ impl Default for SetupPortalState {
625
+ fn default() -> Self {
626
+ Self {
627
+ selected_option: GatewayOption::LocalBundle,
628
+ detecting: false,
629
+ detecting_since: None,
630
+ detection_result: None,
631
+ error: None,
632
+ }
633
+ }
634
+ }
635
+
636
+ impl SetupPortalState {
637
+ /// Re-open Setup without wiping last detection (so the screen still looks “warm” while a refresh runs).
638
+ pub fn soft_open(&mut self) {
639
+ self.error = None;
640
+ }
641
+
642
+ /// Cycle to next/previous option
643
+ /// direction: 1 for down/next, -1 for up/previous
644
+ pub fn cycle_option(&mut self, direction: i8) {
645
+ use GatewayOption::*;
646
+ self.selected_option = match (&self.selected_option, direction) {
647
+ // Forward (Down key, Scroll Down)
648
+ (LocalBundle, 1) => CloudServer,
649
+ (CloudServer, 1) => CustomUrl,
650
+ (CustomUrl, 1) => LocalBundle, // Wrap to top
651
+ // Backward (Up key, Scroll Up)
652
+ (LocalBundle, -1) => CustomUrl, // Wrap to bottom
653
+ (CloudServer, -1) => LocalBundle,
654
+ (CustomUrl, -1) => CloudServer,
655
+ _ => return, // No change for invalid direction
656
+ };
657
+ }
658
+
659
+ /// Clear detection state and errors (full reset; prefer [`soft_open`](Self::soft_open) when reopening Setup).
660
+ #[allow(dead_code)]
661
+ pub fn reset(&mut self) {
662
+ self.detecting = false;
663
+ self.detecting_since = None;
664
+ self.detection_result = None;
665
+ self.error = None;
666
+ }
667
+ }
668
+
669
+ #[derive(Debug, Clone)]
670
+ pub struct AgentListState {
671
+ pub agents: Vec<AgentInfo>,
672
+ pub selected_index: usize,
673
+ pub detail_view: Option<usize>, // None = list view, Some(index) = detail popup
674
+ }
675
+
676
+ impl Default for AgentListState {
677
+ fn default() -> Self {
678
+ Self {
679
+ agents: Vec::new(),
680
+ selected_index: 0,
681
+ detail_view: None,
682
+ }
683
+ }
684
+ }
685
+
686
+ #[derive(Debug, Clone)]
687
+ pub struct AgentInfo {
688
+ pub name: String,
689
+ pub description: Option<String>,
690
+ pub model: String,
691
+ pub provider: String,
692
+ pub system_prompt: Option<String>,
693
+ pub temperature: Option<f32>,
694
+ pub max_tokens: Option<u32>,
695
+ pub tools: Vec<String>,
696
+ }
697
+
698
+ pub struct App {
699
+ pub(crate) state: AppState,
700
+ render_scheduler: RenderScheduler,
701
+ pub(crate) input_debounce: Option<Instant>,
702
+ pub(crate) input_debounce_duration: Duration,
703
+ }
704
+
705
+ impl App {
706
+ pub fn new() -> Self {
707
+ let render_scheduler = RenderScheduler::new();
708
+ let run_mode = render_scheduler.run_mode();
709
+
710
+ // Input debounce: browser 10ms, local 5ms (minimal delay for smooth typing)
711
+ let input_debounce_duration = match run_mode {
712
+ RunMode::Browser => Duration::from_millis(10),
713
+ RunMode::Local => Duration::from_millis(5),
714
+ };
715
+
716
+ let mut state = AppState::default();
717
+ Self::load_cached_data(&mut state);
718
+
719
+ Self {
720
+ state,
721
+ render_scheduler,
722
+ input_debounce: None,
723
+ input_debounce_duration,
724
+ }
725
+ }
726
+
727
+ /// True when the session still has a OS Server URL / connected mode (portal may show linked UI).
728
+ pub fn gateway_session_linked(&self) -> bool {
729
+ self.state.connection_portal.connection_success
730
+ || (matches!(self.state.operation_mode, OperationMode::Connected)
731
+ && self.state.gateway_url.is_some())
732
+ }
733
+
734
+ fn request_gateway_health_check(&mut self, ws_client: Option<&WebSocketClient>) {
735
+ if let Some(ws) = ws_client {
736
+ if let Ok(id) = ws.send_command("gateway.health", None) {
737
+ self.state.pending_gateway_health_id = Some(id);
738
+ self.add_log("[GATEWAY] Re-verifying link (/health + /ready)...".to_string());
739
+ }
740
+ }
741
+ }
742
+
743
+ fn request_gateway_disconnect(&mut self, ws_client: Option<&WebSocketClient>) {
744
+ if let Some(ws) = ws_client {
745
+ match ws.send_command("gateway.disconnect", None) {
746
+ Ok(id) => {
747
+ let short_id = &id[id.len().saturating_sub(8)..];
748
+ self.state
749
+ .logs
750
+ .push_back(format!("[GATEWAY] Disconnecting (id: {})", short_id));
751
+ }
752
+ Err(e) => {
753
+ self.state
754
+ .logs
755
+ .push_back(format!("[ERROR] Failed to disconnect: {}", e));
756
+ }
757
+ }
758
+ } else {
759
+ self.state
760
+ .logs
761
+ .push_back("[ERROR] WebSocket not connected".into());
762
+ }
763
+ }
764
+
765
+ /// Ask CLI to pull Gateway `GET /metrics` and return a condensed snapshot for Portal Monitoring.
766
+ ///
767
+ /// **Intentional command split:** the TUI uses this `gateway.observability` path for the
768
+ /// **Overview** row (full health + Prometheus snapshot in one response). Per-section refresh
769
+ /// uses `begin_monitoring_refresh_request` → `monitoring.refresh` instead. The CLI also maps
770
+ /// `monitoring.refresh` with `section: "overview"` to the same handler for non-TUI callers.
771
+ pub fn begin_portal_observability_request(&mut self, ws: &WebSocketClient) {
772
+ if self.state.pending_gateway_observability_id.is_some() {
773
+ return;
774
+ }
775
+ if self.state.pending_monitoring_refresh_id.is_some()
776
+ || self.state.pending_monitoring_logs_id.is_some()
777
+ || self.state.pending_monitoring_drill_id.is_some()
778
+ {
779
+ return;
780
+ }
781
+ self.state.portal_monitoring.loading = true;
782
+ self.state.portal_monitoring.error = None;
783
+ let nonce = std::time::SystemTime::now()
784
+ .duration_since(std::time::UNIX_EPOCH)
785
+ .map(|d| d.as_millis())
786
+ .unwrap_or(0);
787
+ let data = serde_json::json!({ "nonce": nonce });
788
+ match ws.send_command("gateway.observability", Some(data)) {
789
+ Ok(id) => {
790
+ self.state.pending_gateway_observability_id = Some(id);
791
+ }
792
+ Err(e) => {
793
+ self.state.portal_monitoring.loading = false;
794
+ self.state.portal_monitoring.error = Some(format!("{}", e));
795
+ self.request_immediate_render("portal_obs_send_err");
796
+ }
797
+ }
798
+ }
799
+
800
+ /// Fetch run list from OS Server (`run.list`). `silent` = background poll (no loading spinner).
801
+ pub fn begin_run_list_request(&mut self, ws: &WebSocketClient, silent: bool) {
802
+ if self.state.operation_mode != OperationMode::Connected {
803
+ return;
804
+ }
805
+ if self.state.pending_run_list_id.is_some() {
806
+ return;
807
+ }
808
+ if !silent && self.state.run_manager.loading {
809
+ return;
810
+ }
811
+ let data = serde_json::json!({ "limit": 50 });
812
+ match ws.send_command("run.list", Some(data)) {
813
+ Ok(id) => {
814
+ self.state.pending_run_list_id = Some(id);
815
+ self.state.pending_run_list_silent = silent;
816
+ if !silent {
817
+ self.state.run_manager.loading = true;
818
+ }
819
+ }
820
+ Err(e) => {
821
+ self.add_log(format!("[RUN] run.list failed: {}", e));
822
+ }
823
+ }
824
+ }
825
+
826
+ /// Refresh selected run detail (`run.get`) during live polling.
827
+ pub fn begin_run_get_request(&mut self, ws: &WebSocketClient, run_id: &str) {
828
+ if self.state.operation_mode != OperationMode::Connected {
829
+ return;
830
+ }
831
+ if self.state.pending_run_get_id.is_some() {
832
+ return;
833
+ }
834
+ let data = serde_json::json!({ "runId": run_id });
835
+ if let Ok(id) = ws.send_command("run.get", Some(data)) {
836
+ self.state.pending_run_get_id = Some(id);
837
+ }
838
+ }
839
+
840
+ pub fn open_shield_dashboard(&mut self, ws: Option<&WebSocketClient>) {
841
+ self.push_overlay(Screen::ShieldDashboard);
842
+ self.state
843
+ .logs
844
+ .push_back("[NAV] Opening Shield (production safety layer)...".into());
845
+ if self.state.operation_mode == OperationMode::Connected {
846
+ if let Some(ws) = ws {
847
+ self.begin_shield_load_request(ws, false);
848
+ }
849
+ } else {
850
+ self.state.shield_dashboard.loading = false;
851
+ self.state.shield_dashboard.error = Some(
852
+ "Connect to OS Server first (connect portal).".to_string(),
853
+ );
854
+ }
855
+ self.request_immediate_render("open_shield_dashboard");
856
+ }
857
+
858
+ pub fn begin_shield_load_request(&mut self, ws: &WebSocketClient, force: bool) {
859
+ if self.state.operation_mode != OperationMode::Connected {
860
+ return;
861
+ }
862
+ if !force && self.state.pending_shield_load_id.is_some() {
863
+ return;
864
+ }
865
+ if force {
866
+ self.state.pending_shield_load_id = None;
867
+ }
868
+ self.state.shield_dashboard.mark_loading();
869
+ self.state.shield_dashboard.error = None;
870
+ if let Ok(id) = ws.send_command("shield.load", None) {
871
+ self.state.pending_shield_load_id = Some(id);
872
+ } else {
873
+ self.state.shield_dashboard.fail_loading(
874
+ "Failed to send shield.load to CLI.".to_string(),
875
+ );
876
+ }
877
+ }
878
+
879
+ pub fn open_run_from_shield_block(&mut self, ws: Option<&WebSocketClient>, run_id: &str) {
880
+ // Ensure Shield stays on the stack beneath Run Manager so ESC returns here.
881
+ if !self
882
+ .state
883
+ .navigation
884
+ .overlay_stack
885
+ .iter()
886
+ .any(|s| *s == Screen::ShieldDashboard)
887
+ {
888
+ self.push_overlay(Screen::ShieldDashboard);
889
+ }
890
+ self.state.run_manager.return_to_overlay = Some(Screen::ShieldDashboard);
891
+ self.push_overlay(Screen::RunManager);
892
+ self.state.run_manager.detail_run_id = Some(run_id.to_string());
893
+ self.add_log(format!(
894
+ "[SHIELD] Opening blocked run {} in Run Manager",
895
+ &run_id[run_id.len().saturating_sub(8)..]
896
+ ));
897
+ if let Some(ws) = ws {
898
+ self.begin_run_list_request(ws, false);
899
+ self.begin_run_get_request(ws, run_id);
900
+ }
901
+ self.request_immediate_render("shield_open_run");
902
+ }
903
+
904
+ pub fn open_sentinel_config(&mut self, ws: Option<&WebSocketClient>) {
905
+ self.state.pending_sentinel_load_id = None;
906
+ self.state.pending_sentinel_apply_id = None;
907
+ self.push_overlay(Screen::SentinelConfig);
908
+ self.state
909
+ .logs
910
+ .push_back("[NAV] Opening Sentinel Configuration...".into());
911
+ if self.state.operation_mode == OperationMode::Connected {
912
+ if let Some(ws) = ws {
913
+ self.begin_sentinel_load_request(ws);
914
+ }
915
+ } else {
916
+ self.state.sentinel_config.error = Some(
917
+ "Connect to OS Server first (connect portal).".to_string(),
918
+ );
919
+ }
920
+ self.request_immediate_render("open_sentinel_config");
921
+ }
922
+
923
+ pub fn begin_sentinel_load_request(&mut self, ws: &WebSocketClient) {
924
+ if self.state.operation_mode != OperationMode::Connected {
925
+ return;
926
+ }
927
+ if self.state.pending_sentinel_load_id.is_some() {
928
+ return;
929
+ }
930
+ self.state.sentinel_config.loading = true;
931
+ self.state.sentinel_config.error = None;
932
+ self.state.sentinel_config.status_message = None;
933
+ if let Ok(id) = ws.send_command("sentinel.load", None) {
934
+ self.state.pending_sentinel_load_id = Some(id);
935
+ } else {
936
+ self.state.sentinel_config.loading = false;
937
+ }
938
+ }
939
+
940
+ pub fn begin_sentinel_apply_request(&mut self, ws: &WebSocketClient, template_key: &str) {
941
+ if self.state.operation_mode != OperationMode::Connected {
942
+ return;
943
+ }
944
+ if self.state.pending_sentinel_load_id.is_some()
945
+ || self.state.pending_sentinel_apply_id.is_some()
946
+ {
947
+ return;
948
+ }
949
+ self.state.sentinel_config.applying = true;
950
+ self.state.sentinel_config.error = None;
951
+ self.state.sentinel_config.status_message = None;
952
+ let data = serde_json::json!({ "template": template_key });
953
+ if let Ok(id) = ws.send_command("sentinel.apply", Some(data)) {
954
+ self.state.pending_sentinel_apply_id = Some(id);
955
+ } else {
956
+ self.state.sentinel_config.applying = false;
957
+ }
958
+ }
959
+
960
+ pub fn begin_sentinel_apply_custom_request(&mut self, ws: &WebSocketClient, draft: &crate::ui::sentinel_config::SentinelCurrentConfig) {
961
+ if self.state.operation_mode != OperationMode::Connected {
962
+ return;
963
+ }
964
+ if self.state.pending_sentinel_load_id.is_some()
965
+ || self.state.pending_sentinel_apply_id.is_some()
966
+ {
967
+ return;
968
+ }
969
+ self.state.sentinel_config.applying = true;
970
+ self.state.sentinel_config.error = None;
971
+ self.state.sentinel_config.status_message = None;
972
+ let data = serde_json::json!({
973
+ "enabled": draft.enabled,
974
+ "runIdleMs": draft.idle_ms,
975
+ "runMaxDurationMs": draft.duration_ms,
976
+ "runMaxTokens": draft.max_tokens,
977
+ "runMaxCost": draft.max_cost,
978
+ "loopWindow": draft.loop_window,
979
+ "loopMax": draft.loop_max,
980
+ });
981
+ if let Ok(id) = ws.send_command("sentinel.applyCustom", Some(data)) {
982
+ self.state.pending_sentinel_apply_id = Some(id);
983
+ } else {
984
+ self.state.sentinel_config.applying = false;
985
+ }
986
+ }
987
+
988
+ /// Phase 2: refresh one Portal Monitoring section via CLI (`monitoring.refresh` → Gateway APIs).
989
+ pub fn begin_monitoring_refresh_request(
990
+ &mut self,
991
+ ws: &WebSocketClient,
992
+ section: MonitoringSection,
993
+ ) {
994
+ if self.state.pending_gateway_observability_id.is_some()
995
+ || self.state.pending_monitoring_refresh_id.is_some()
996
+ || self.state.pending_monitoring_logs_id.is_some()
997
+ || self.state.pending_monitoring_drill_id.is_some()
998
+ {
999
+ return;
1000
+ }
1001
+ let nonce = std::time::SystemTime::now()
1002
+ .duration_since(std::time::UNIX_EPOCH)
1003
+ .map(|d| d.as_millis())
1004
+ .unwrap_or(0);
1005
+ let slug = section.cli_slug();
1006
+ let data = serde_json::json!({ "section": slug, "nonce": nonce });
1007
+ match ws.send_command("monitoring.refresh", Some(data)) {
1008
+ Ok(id) => {
1009
+ self.state.pending_monitoring_refresh_id = Some(id);
1010
+ self.state.portal_monitoring.section_refresh_loading = Some(section);
1011
+ self.state.portal_monitoring.error = None;
1012
+ }
1013
+ Err(e) => {
1014
+ self.state.portal_monitoring.section_refresh_loading = None;
1015
+ self.state.portal_monitoring.error = Some(format!("{}", e));
1016
+ self.request_immediate_render("monitoring_refresh_send_err");
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ /// Phase 2: fetch Gateway log buffer (`monitoring.logs`).
1022
+ pub fn begin_monitoring_logs_request(&mut self, ws: &WebSocketClient) {
1023
+ if self.state.pending_gateway_observability_id.is_some()
1024
+ || self.state.pending_monitoring_refresh_id.is_some()
1025
+ || self.state.pending_monitoring_logs_id.is_some()
1026
+ || self.state.pending_monitoring_drill_id.is_some()
1027
+ {
1028
+ return;
1029
+ }
1030
+ let nonce = std::time::SystemTime::now()
1031
+ .duration_since(std::time::UNIX_EPOCH)
1032
+ .map(|d| d.as_millis())
1033
+ .unwrap_or(0);
1034
+ let data = serde_json::json!({ "limit": 100u32, "nonce": nonce });
1035
+ match ws.send_command("monitoring.logs", Some(data)) {
1036
+ Ok(id) => {
1037
+ self.state.pending_monitoring_logs_id = Some(id);
1038
+ self.state.portal_monitoring.logs_fetch_loading = true;
1039
+ self.state.portal_monitoring.error = None;
1040
+ }
1041
+ Err(e) => {
1042
+ self.state.portal_monitoring.logs_fetch_loading = false;
1043
+ self.state.portal_monitoring.error = Some(format!("{}", e));
1044
+ self.request_immediate_render("monitoring_logs_send_err");
1045
+ }
1046
+ }
1047
+ }
1048
+
1049
+ /// Phase 3: Gateway drill-down (`monitoring.drill`) for Metrics sub-panels (HTTP / Runs / Queue / SSE).
1050
+ pub fn begin_monitoring_drill_request(
1051
+ &mut self,
1052
+ ws: &WebSocketClient,
1053
+ panel: MetricsDrillPanel,
1054
+ ) {
1055
+ if self.state.pending_gateway_observability_id.is_some()
1056
+ || self.state.pending_monitoring_refresh_id.is_some()
1057
+ || self.state.pending_monitoring_logs_id.is_some()
1058
+ || self.state.pending_monitoring_drill_id.is_some()
1059
+ {
1060
+ return;
1061
+ }
1062
+ let nonce = std::time::SystemTime::now()
1063
+ .duration_since(std::time::UNIX_EPOCH)
1064
+ .map(|d| d.as_millis())
1065
+ .unwrap_or(0);
1066
+ let target = panel.cli_target();
1067
+ let data = serde_json::json!({ "target": target, "nonce": nonce });
1068
+ match ws.send_command("monitoring.drill", Some(data)) {
1069
+ Ok(id) => {
1070
+ self.state.pending_monitoring_drill_id = Some(id);
1071
+ self.state.portal_monitoring.metrics_drill = Some(panel);
1072
+ self.state.portal_monitoring.metrics_drill_loading = true;
1073
+ self.state.portal_monitoring.metrics_drill_lines.clear();
1074
+ self.state.portal_monitoring.error = None;
1075
+ }
1076
+ Err(e) => {
1077
+ self.state.portal_monitoring.metrics_drill_loading = false;
1078
+ self.state.portal_monitoring.error = Some(format!("{}", e));
1079
+ self.request_immediate_render("monitoring_drill_send_err");
1080
+ }
1081
+ }
1082
+ }
1083
+
1084
+ /// Phase 3: dependency pools + queue counts (`dependencies/detail`).
1085
+ pub fn begin_dependencies_detail_drill(&mut self, ws: &WebSocketClient) {
1086
+ if self.state.pending_gateway_observability_id.is_some()
1087
+ || self.state.pending_monitoring_refresh_id.is_some()
1088
+ || self.state.pending_monitoring_logs_id.is_some()
1089
+ || self.state.pending_monitoring_drill_id.is_some()
1090
+ {
1091
+ return;
1092
+ }
1093
+ let nonce = std::time::SystemTime::now()
1094
+ .duration_since(std::time::UNIX_EPOCH)
1095
+ .map(|d| d.as_millis())
1096
+ .unwrap_or(0);
1097
+ let data = serde_json::json!({ "target": "dependencies_detail", "nonce": nonce });
1098
+ match ws.send_command("monitoring.drill", Some(data)) {
1099
+ Ok(id) => {
1100
+ self.state.pending_monitoring_drill_id = Some(id);
1101
+ self.state.portal_monitoring.section_refresh_loading =
1102
+ Some(MonitoringSection::Dependencies);
1103
+ self.state.portal_monitoring.error = None;
1104
+ }
1105
+ Err(e) => {
1106
+ self.state.portal_monitoring.section_refresh_loading = None;
1107
+ self.state.portal_monitoring.error = Some(format!("{}", e));
1108
+ self.request_immediate_render("deps_drill_send_err");
1109
+ }
1110
+ }
1111
+ }
1112
+
1113
+ /// Phase 4: run local CLI/host diagnostics for the System section.
1114
+ pub fn begin_system_diagnostics_request(&mut self, ws: &WebSocketClient) {
1115
+ if self.state.pending_gateway_observability_id.is_some()
1116
+ || self.state.pending_monitoring_refresh_id.is_some()
1117
+ || self.state.pending_monitoring_logs_id.is_some()
1118
+ || self.state.pending_monitoring_drill_id.is_some()
1119
+ {
1120
+ return;
1121
+ }
1122
+ let nonce = std::time::SystemTime::now()
1123
+ .duration_since(std::time::UNIX_EPOCH)
1124
+ .map(|d| d.as_millis())
1125
+ .unwrap_or(0);
1126
+ let data = serde_json::json!({ "target": "system_diagnostics", "nonce": nonce });
1127
+ match ws.send_command("monitoring.drill", Some(data)) {
1128
+ Ok(id) => {
1129
+ self.state.pending_monitoring_drill_id = Some(id);
1130
+ self.state.portal_monitoring.section_refresh_loading =
1131
+ Some(MonitoringSection::System);
1132
+ self.state.portal_monitoring.error = None;
1133
+ }
1134
+ Err(e) => {
1135
+ self.state.portal_monitoring.section_refresh_loading = None;
1136
+ self.state.portal_monitoring.error = Some(format!("{}", e));
1137
+ self.request_immediate_render("system_diagnostics_send_err");
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ pub fn export_portal_monitoring_snapshot(&mut self) -> anyhow::Result<String> {
1143
+ let mut overrides = serde_json::Map::new();
1144
+ for (section, lines) in &self.state.portal_monitoring.section_overrides {
1145
+ overrides.insert(section.cli_slug().to_string(), serde_json::json!(lines));
1146
+ }
1147
+ let payload = serde_json::json!({
1148
+ "exportedAt": wall_clock_hms(),
1149
+ "gatewayUrl": self.state.gateway_url.clone(),
1150
+ "lastUpdated": self.state.portal_monitoring.last_updated.clone(),
1151
+ "contentLines": self.state.portal_monitoring.content_lines.clone(),
1152
+ "sectionOverrides": overrides,
1153
+ "metricHistory": self.state.portal_monitoring.metric_history.clone(),
1154
+ "dependencyAlerts": self.state.portal_monitoring.dependency_alerts.clone(),
1155
+ "trendView": self.state.portal_monitoring.trend_view,
1156
+ });
1157
+ let unix = std::time::SystemTime::now()
1158
+ .duration_since(std::time::UNIX_EPOCH)
1159
+ .map(|d| d.as_secs())
1160
+ .unwrap_or(0);
1161
+ let mut path = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir());
1162
+ path.push(format!("4runr-monitoring-export-{unix}.json"));
1163
+ let body = serde_json::to_string_pretty(&payload)?;
1164
+ std::fs::write(&path, body)?;
1165
+ let display = path.display().to_string();
1166
+ self.state.portal_monitoring.last_export_path = Some(display.clone());
1167
+ Ok(display)
1168
+ }
1169
+
1170
+ pub fn scroll_portal_monitoring_by(&mut self, delta: isize) {
1171
+ use crate::ui::portal_monitoring::portal_monitoring_scroll_max;
1172
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
1173
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
1174
+ let max_scroll = portal_monitoring_scroll_max(&self.state, sw, inner_h);
1175
+ let current = self.state.portal_monitoring.scroll_offset;
1176
+ self.state.portal_monitoring.scroll_offset = if delta < 0 {
1177
+ current.saturating_sub(delta.unsigned_abs())
1178
+ } else {
1179
+ current.saturating_add(delta as usize).min(max_scroll)
1180
+ };
1181
+ self.request_immediate_render("portal_obs_scroll");
1182
+ }
1183
+
1184
+ fn cancel_portal_monitoring_requests(&mut self) {
1185
+ self.state.pending_gateway_observability_id = None;
1186
+ self.state.pending_monitoring_refresh_id = None;
1187
+ self.state.pending_monitoring_logs_id = None;
1188
+ self.state.pending_monitoring_drill_id = None;
1189
+ self.state.portal_monitoring.loading = false;
1190
+ self.state.portal_monitoring.section_refresh_loading = None;
1191
+ self.state.portal_monitoring.logs_fetch_loading = false;
1192
+ self.state.portal_monitoring.metrics_drill_loading = false;
1193
+ }
1194
+
1195
+ /// CLI ↔ TUI WebSocket lost: drop Gateway link UI and leave Portal Monitoring / Connection Portal so the session does not appear still "connected".
1196
+ pub fn on_cli_backend_disconnect(&mut self) {
1197
+ use crate::screens::Screen;
1198
+ self.state.operation_mode = OperationMode::Local;
1199
+ self.state.gateway_url = None;
1200
+ self.state.connected = false;
1201
+ self.state.gateway_healthy = false;
1202
+ self.state.gateway_db_summary = None;
1203
+ self.state.pending_gateway_connect_id = None;
1204
+ self.state.pending_gateway_health_id = None;
1205
+ self.state.pending_gateway_observability_id = None;
1206
+ self.state.pending_monitoring_refresh_id = None;
1207
+ self.state.pending_monitoring_logs_id = None;
1208
+ self.state.pending_monitoring_drill_id = None;
1209
+ self.state.portal_observability_last_poll = None;
1210
+ self.state.portal_monitoring = PortalMonitoringState::default();
1211
+ self.state.advanced_monitoring = AdvancedMonitoringState::default();
1212
+ self.state.connection_portal.finish_connecting();
1213
+ self.state.connection_portal.connection_success = false;
1214
+ self.state.connection_portal.connecting = false;
1215
+ self.state.run_manager.clear_runs();
1216
+
1217
+ let screen = self.state.navigation.current_screen();
1218
+ if matches!(
1219
+ screen,
1220
+ &Screen::PortalMonitoring | &Screen::ConnectionPortal
1221
+ ) {
1222
+ self.state.navigation.navigate_to_base(Screen::Main);
1223
+ self.request_immediate_render("cli_backend_disconnect_nav_main");
1224
+ } else {
1225
+ self.request_immediate_render("cli_backend_disconnect");
1226
+ }
1227
+ }
1228
+
1229
+ /// Load cached data on startup
1230
+ fn load_cached_data(state: &mut AppState) {
1231
+ if let Some(cache) = &state.cache {
1232
+ // Load cached agents
1233
+ let agents = cache.get_agents();
1234
+ if !agents.is_empty() {
1235
+ state.capabilities = agents.iter().map(|a| a.name.clone()).collect();
1236
+ state.cache_loaded = true;
1237
+ }
1238
+
1239
+ // Load cached system status
1240
+ if let Some(status) = cache.get_system_status() {
1241
+ state.network_status = if status.connected {
1242
+ "Connected (cached)".to_string()
1243
+ } else {
1244
+ "Disconnected (cached)".to_string()
1245
+ };
1246
+ state.posture_status = format!("{} (cached)", status.posture);
1247
+ }
1248
+ }
1249
+ }
1250
+
1251
+ pub fn request_render(&mut self, reason: &str) -> bool {
1252
+ self.render_scheduler.request_render(reason)
1253
+ }
1254
+
1255
+ /// Request immediate render (bypass throttling) - for critical input events
1256
+ pub fn request_immediate_render(&mut self, reason: &str) -> bool {
1257
+ self.render_scheduler.request_immediate_render(reason)
1258
+ }
1259
+
1260
+ pub fn should_render(&mut self) -> bool {
1261
+ self.render_scheduler.should_render()
1262
+ }
1263
+
1264
+ pub fn record_render(&mut self, duration_ms: u64) {
1265
+ self.state.render_count += 1;
1266
+ self.state.render_durations.push_back(duration_ms);
1267
+ if self.state.render_durations.len() > 60 {
1268
+ self.state.render_durations.pop_front();
1269
+ }
1270
+ self.state.last_render_time = Instant::now();
1271
+ }
1272
+
1273
+ pub fn run_mode(&self) -> RunMode {
1274
+ self.render_scheduler.run_mode()
1275
+ }
1276
+
1277
+ pub fn render_scheduler_stats(&self) -> (u64, u64) {
1278
+ (
1279
+ self.render_scheduler.render_scheduled_count(),
1280
+ self.render_scheduler.min_render_interval_ms(),
1281
+ )
1282
+ }
1283
+
1284
+ /// IMPORTANT: Only call tick() when poll() times out (NO keyboard input)!
1285
+ /// Otherwise animations will flash weirdly when typing.
1286
+ ///
1287
+ /// NOTE: Uptime is now tracked via Instant::now() in main loop,
1288
+ /// so we don't increment it here anymore.
1289
+ pub fn tick(&mut self) {
1290
+ self.state.tick = self.state.tick.wrapping_add(1);
1291
+
1292
+ // Update spinner frame (for "Processing..." indicator only)
1293
+ // Cycle through 8 braille spinner frames
1294
+ self.state.spinner_frame = (self.state.spinner_frame + 1) % 8;
1295
+
1296
+ // Boot timeline: update progress every ~150-250ms
1297
+ if self.state.mode == AppMode::Boot && !self.state.boot_done {
1298
+ let elapsed = self.state.boot_started_at.elapsed();
1299
+ let boot_steps = vec![
1300
+ (Duration::from_millis(200), "Initializing gateway…"),
1301
+ (Duration::from_millis(500), "Loading policies…"),
1302
+ (Duration::from_millis(800), "Agent registry online…"),
1303
+ (Duration::from_millis(1100), "Telemetry connected…"),
1304
+ (Duration::from_millis(1400), "System ready."),
1305
+ ];
1306
+
1307
+ let mut current_progress = 0;
1308
+ let mut step_idx = 0;
1309
+
1310
+ for (i, (delay, msg)) in boot_steps.iter().enumerate() {
1311
+ if elapsed >= *delay {
1312
+ step_idx = i + 1;
1313
+ current_progress = ((i + 1) * 100 / boot_steps.len()) as u16;
1314
+
1315
+ // Add line if not already added
1316
+ if self.state.boot_lines.len() <= i {
1317
+ self.state.boot_lines.push_back(msg.to_string());
1318
+ self.request_render("boot_line");
1319
+ }
1320
+ }
1321
+ }
1322
+
1323
+ self.state.boot_progress = current_progress.min(100);
1324
+
1325
+ // Mark boot done after last step
1326
+ if step_idx >= boot_steps.len() && self.state.boot_progress >= 100 {
1327
+ self.state.boot_done = true;
1328
+ self.request_render("boot_done");
1329
+ }
1330
+ }
1331
+
1332
+ // NOTE: pulse_frame is NOT updated here - we use static dots for status indicators
1333
+ // to prevent flashing when typing. Only use animated pulse for non-status elements.
1334
+ }
1335
+
1336
+ pub fn handle_input(
1337
+ &mut self,
1338
+ key: KeyEvent,
1339
+ _io: &mut IoHandler,
1340
+ ws_client: Option<&WebSocketClient>,
1341
+ ) -> anyhow::Result<bool> {
1342
+ use crossterm::event::KeyModifiers;
1343
+
1344
+ // === EXIT SHORTCUTS (always checked first) ===
1345
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
1346
+ match key.code {
1347
+ KeyCode::Char('c') | KeyCode::Char('q') | KeyCode::Char('d') => {
1348
+ return Ok(true); // Exit
1349
+ }
1350
+ _ => return Ok(false), // Ignore other Ctrl combos
1351
+ }
1352
+ }
1353
+
1354
+ if key.code == KeyCode::F(10) {
1355
+ return Ok(true); // Exit
1356
+ }
1357
+
1358
+ // === BOOT MODE: Any key to continue ===
1359
+ if self.state.mode == AppMode::Boot && self.state.boot_done {
1360
+ // Any key press switches to main dashboard
1361
+ self.navigate_to(Screen::Main);
1362
+ return Ok(false);
1363
+ }
1364
+
1365
+ // In boot mode, ignore other input until boot is done
1366
+ if self.state.mode == AppMode::Boot {
1367
+ return Ok(false);
1368
+ }
1369
+
1370
+ // F12 - Toggle perf overlay
1371
+ if key.code == KeyCode::F(12) {
1372
+ self.state.perf_overlay = !self.state.perf_overlay;
1373
+ self.request_render("perf_toggle");
1374
+ return Ok(false);
1375
+ }
1376
+
1377
+ // === AGENT BUILDER INPUT HANDLING ===
1378
+ if self.state.navigation.current_screen() == &Screen::AgentBuilder {
1379
+ return self.handle_agent_builder_input(key, ws_client);
1380
+ }
1381
+
1382
+ // === RUN MANAGER INPUT HANDLING ===
1383
+ if self.state.navigation.current_screen() == &Screen::RunManager {
1384
+ return self.handle_run_manager_input(key, ws_client);
1385
+ }
1386
+
1387
+ // === SENTINEL CONFIG INPUT HANDLING ===
1388
+ if self.state.navigation.current_screen() == &Screen::SentinelConfig {
1389
+ return self.handle_sentinel_config_input(key, ws_client);
1390
+ }
1391
+
1392
+ // === SHIELD DASHBOARD INPUT HANDLING ===
1393
+ if self.state.navigation.current_screen() == &Screen::ShieldDashboard {
1394
+ return self.handle_shield_dashboard_input(key, ws_client);
1395
+ }
1396
+
1397
+ // === SETTINGS INPUT HANDLING ===
1398
+ if self.state.navigation.current_screen() == &Screen::Settings {
1399
+ return self.handle_settings_input(key);
1400
+ }
1401
+
1402
+ // === CONNECTION PORTAL INPUT HANDLING ===
1403
+ if self.state.navigation.current_screen() == &Screen::ConnectionPortal {
1404
+ return self.handle_connection_portal_input(key, ws_client);
1405
+ }
1406
+
1407
+ // === PORTAL MONITORING (Gateway traffic view) ===
1408
+ if self.state.navigation.current_screen() == &Screen::PortalMonitoring {
1409
+ return self.handle_portal_monitoring_input(key, ws_client);
1410
+ }
1411
+
1412
+ // === SETUP PORTAL INPUT HANDLING ===
1413
+ if self.state.navigation.current_screen() == &Screen::SetupPortal {
1414
+ return self.handle_setup_portal_input(key, ws_client);
1415
+ }
1416
+
1417
+ // === AGENT LIST INPUT HANDLING ===
1418
+ if self.state.navigation.current_screen() == &Screen::AgentList {
1419
+ return self.handle_agent_list_input(key, ws_client);
1420
+ }
1421
+
1422
+ // === MAIN INPUT HANDLING ===
1423
+ match key.code {
1424
+ // F2 - Open Setup Portal (Gateway options) from main screen
1425
+ KeyCode::F(2) => {
1426
+ self.state.navigation.navigate_to_base(Screen::SetupPortal);
1427
+ self.state.setup_portal.soft_open();
1428
+ if let Some(ws) = ws_client {
1429
+ self.state.setup_portal.detecting = true;
1430
+ self.state.setup_portal.detecting_since = Some(std::time::Instant::now());
1431
+ if let Ok(id) = ws.send_command("setup.detect", None) {
1432
+ self.state.pending_setup_detect_id = Some(id);
1433
+ self.add_log("[SETUP] Detecting Gateway options...".to_string());
1434
+ } else {
1435
+ self.state.setup_portal.detecting = false;
1436
+ self.state.setup_portal.detecting_since = None;
1437
+ self.state.setup_portal.error =
1438
+ Some("Failed to start detection".to_string());
1439
+ }
1440
+ } else {
1441
+ self.state.setup_portal.error = Some(
1442
+ "CLI backend not connected — auto-detect skipped. Choose an option and press Enter; set GATEWAY_URL in the shell if needed."
1443
+ .into(),
1444
+ );
1445
+ }
1446
+ self.state
1447
+ .logs
1448
+ .push_back("[NAV] Opening Setup Portal (F2)...".into());
1449
+ self.request_immediate_render("open_setup_portal");
1450
+ }
1451
+ // Typing - ALL characters go to command input (immediate render for responsiveness)
1452
+ KeyCode::Char(c) => {
1453
+ // CRITICAL: Always set focused when typing to show cursor
1454
+ self.state.command_focused = true;
1455
+ self.state.command_input.push(c);
1456
+ // OPTIMIZATION: Immediate render for instant typing feedback
1457
+ self.request_immediate_render("typing_input");
1458
+ }
1459
+
1460
+ // Submit command
1461
+ KeyCode::Enter => {
1462
+ if !self.state.command_input.is_empty() {
1463
+ let cmd: String = self.state.command_input.drain(..).collect();
1464
+ self.state.command_focused = false;
1465
+ self.input_debounce = None;
1466
+
1467
+ match cmd.to_lowercase().as_str() {
1468
+ "quit" | "exit" => return Ok(true),
1469
+ "clear" => self.state.logs.clear(),
1470
+ "help" => {
1471
+ // Display help in operations log - clean and organized
1472
+ // NOTE: VecDeque displays newest items first, so we push in REVERSE order
1473
+ // (Last item pushed appears at top of log)
1474
+
1475
+ // Push Keyboard Shortcuts FIRST (will appear at BOTTOM)
1476
+ self.state.logs.push_back(
1477
+ " Settings: ↑/↓=Navigate, Space=Toggle, Enter=Save"
1478
+ .to_string(),
1479
+ );
1480
+ self.state.logs.push_back(
1481
+ " Run Manager: ↑/↓=Navigate, F=Filter, S=Sort, R=Refresh"
1482
+ .to_string(),
1483
+ );
1484
+ self.state.logs.push_back(
1485
+ " Agent Builder: Enter=Next, Backspace=Back, ESC=Cancel"
1486
+ .to_string(),
1487
+ );
1488
+ self.state.logs.push_back(
1489
+ "────────────────────────────────────────────────────────────────"
1490
+ .to_string(),
1491
+ );
1492
+ self.state
1493
+ .logs
1494
+ .push_back("⌨️ KEYBOARD SHORTCUTS".to_string());
1495
+ self.state.logs.push_back("".to_string());
1496
+
1497
+ // WebSocket Commands
1498
+ self.state.logs.push_back(
1499
+ " tool.list - List available tools".to_string(),
1500
+ );
1501
+ self.state.logs.push_back(
1502
+ " run.list - List runs (requires gateway)".to_string(),
1503
+ );
1504
+ self.state
1505
+ .logs
1506
+ .push_back(" system.status - Get system status".to_string());
1507
+ self.state.logs.push_back(
1508
+ " agent.delete - Delete agent (data: {name})".to_string(),
1509
+ );
1510
+ self.state.logs.push_back(
1511
+ " agent.create - Create new agent (use Agent Builder)"
1512
+ .to_string(),
1513
+ );
1514
+ self.state.logs.push_back(
1515
+ " agent.get - Get agent details (data: {name})"
1516
+ .to_string(),
1517
+ );
1518
+ self.state
1519
+ .logs
1520
+ .push_back(" agent.list - List all agents".to_string());
1521
+ self.state.logs.push_back(
1522
+ "────────────────────────────────────────────────────────────────"
1523
+ .to_string(),
1524
+ );
1525
+ self.state.logs.push_back(
1526
+ "🌐 WEBSOCKET COMMANDS (requires connection)".to_string(),
1527
+ );
1528
+ self.state.logs.push_back("".to_string());
1529
+
1530
+ // Local Commands
1531
+ self.state.logs.push_back(
1532
+ " :perf - Show performance stats".to_string(),
1533
+ );
1534
+ self.state
1535
+ .logs
1536
+ .push_back(" help - Show this help".to_string());
1537
+ self.state.logs.push_back(
1538
+ " clear - Clear operations log".to_string(),
1539
+ );
1540
+ self.state
1541
+ .logs
1542
+ .push_back(" quit, exit - Exit application".to_string());
1543
+ self.state.logs.push_back(
1544
+ "────────────────────────────────────────────────────────────────"
1545
+ .to_string(),
1546
+ );
1547
+ self.state.logs.push_back("💻 LOCAL COMMANDS".to_string());
1548
+ self.state.logs.push_back("".to_string());
1549
+
1550
+ // Navigation Commands
1551
+ self.state.logs.push_back(" disconnect - Disconnect from OS Server (return to LOCAL mode)".to_string());
1552
+ self.state.logs.push_back(" portal monitoring - Gateway /metrics traffic (R refresh, ↑↓ scroll)".to_string());
1553
+ self.state.logs.push_back(" connect portal - Open Connection Portal (connect to OS Server)".to_string());
1554
+ self.state.logs.push_back(
1555
+ " a, agents - Open Agent List (view, select, delete)"
1556
+ .to_string(),
1557
+ );
1558
+ self.state.logs.push_back(
1559
+ " config, settings - Open Settings (mode, AI provider)"
1560
+ .to_string(),
1561
+ );
1562
+ self.state.logs.push_back(
1563
+ " runs - Open Run Manager (list, filter, sort)"
1564
+ .to_string(),
1565
+ );
1566
+ self.state.logs.push_back(
1567
+ " sentinel - Configure Sentinel policies (templates)"
1568
+ .to_string(),
1569
+ );
1570
+ self.state.logs.push_back(
1571
+ " shield - Shield safety dashboard (live metrics + enforcement)"
1572
+ .to_string(),
1573
+ );
1574
+ self.state.logs.push_back(
1575
+ " build - Open Agent Builder (6-step wizard)"
1576
+ .to_string(),
1577
+ );
1578
+ self.state.logs.push_back(
1579
+ "────────────────────────────────────────────────────────────────"
1580
+ .to_string(),
1581
+ );
1582
+ self.state
1583
+ .logs
1584
+ .push_back("🧭 NAVIGATION COMMANDS".to_string());
1585
+ self.state.logs.push_back("".to_string());
1586
+
1587
+ // Push Header LAST (will appear at TOP)
1588
+ self.state.logs.push_back(
1589
+ "════════════════════════════════════════════════════════════════"
1590
+ .to_string(),
1591
+ );
1592
+ self.state
1593
+ .logs
1594
+ .push_back("📖 4RUNR AI AGENT OS - COMMAND REFERENCE".to_string());
1595
+ self.state.logs.push_back(
1596
+ "════════════════════════════════════════════════════════════════"
1597
+ .to_string(),
1598
+ );
1599
+
1600
+ self.request_render("help_command");
1601
+ }
1602
+ ":perf" => {
1603
+ // Perf self-check command
1604
+ let rps = if !self.state.render_durations.is_empty() {
1605
+ let avg_ms: f64 = self.state.render_durations.iter().sum::<u64>()
1606
+ as f64
1607
+ / self.state.render_durations.len() as f64;
1608
+ if avg_ms > 0.0 {
1609
+ (1000.0 / avg_ms) as u64
1610
+ } else {
1611
+ 0
1612
+ }
1613
+ } else {
1614
+ 0
1615
+ };
1616
+
1617
+ let mode_str = match self.run_mode() {
1618
+ RunMode::Browser => "browser",
1619
+ RunMode::Local => "local",
1620
+ };
1621
+
1622
+ let (scheduled_count, interval_ms) = self.render_scheduler_stats();
1623
+
1624
+ self.state.logs.push_back(format!(
1625
+ "[PERF] Mode: {} | RPS: {} | Render interval: {}ms | Scheduled: {}",
1626
+ mode_str, rps, interval_ms, scheduled_count
1627
+ ));
1628
+ }
1629
+ // Navigation commands
1630
+ "build" | "build new" | "agent new" => {
1631
+ self.push_overlay(Screen::AgentBuilder);
1632
+ self.state
1633
+ .logs
1634
+ .push_back("[NAV] Opening Agent Builder...".into());
1635
+ }
1636
+ "runs" | "run list" | "run manager" => {
1637
+ self.push_overlay(Screen::RunManager);
1638
+ self.state
1639
+ .logs
1640
+ .push_back("[NAV] Opening Run Manager...".into());
1641
+ if self.state.operation_mode == OperationMode::Connected {
1642
+ if let Some(ws) = ws_client {
1643
+ self.begin_run_list_request(ws, false);
1644
+ }
1645
+ } else {
1646
+ self.add_log(
1647
+ "[RUN] Connect to OS Server (connect portal) to load live runs."
1648
+ .to_string(),
1649
+ );
1650
+ }
1651
+ }
1652
+ "sentinel" | "sentinel config" | "sentinel policies" => {
1653
+ self.open_sentinel_config(ws_client);
1654
+ }
1655
+ "shield" | "shield status" | "shield dashboard" => {
1656
+ self.open_shield_dashboard(ws_client);
1657
+ }
1658
+ "config" | "settings" => {
1659
+ self.push_overlay(Screen::Settings);
1660
+ self.state
1661
+ .logs
1662
+ .push_back("[NAV] Opening Settings...".into());
1663
+ }
1664
+ "a" | "agents" | "agent list" => {
1665
+ self.push_overlay(Screen::AgentList);
1666
+ self.state
1667
+ .logs
1668
+ .push_back("[NAV] Opening Agent List...".into());
1669
+ // Request agent list from backend
1670
+ if let Some(ws) = ws_client {
1671
+ if let Ok(list_id) = ws.send_command("agent.list", None) {
1672
+ self.state.pending_agent_list_id = Some(list_id);
1673
+ }
1674
+ }
1675
+ }
1676
+ "portal monitoring" | "portal monitor" => {
1677
+ if self.state.operation_mode == OperationMode::Connected {
1678
+ self.state
1679
+ .navigation
1680
+ .navigate_to_base(Screen::PortalMonitoring);
1681
+ self.state.advanced_monitoring =
1682
+ AdvancedMonitoringState::new(self.state.gateway_url.clone());
1683
+ self.state.portal_monitoring.scroll_offset = 0;
1684
+ self.state
1685
+ .logs
1686
+ .push_back("[NAV] Opening Portal Monitoring...".into());
1687
+ if let Some(ws) = ws_client {
1688
+ self.begin_portal_observability_request(ws);
1689
+ }
1690
+ self.request_immediate_render("open_portal_monitoring");
1691
+ } else {
1692
+ self.state.logs.push_back(
1693
+ "[NAV] Connect to a Gateway first (connect portal), then open Portal Monitoring."
1694
+ .into(),
1695
+ );
1696
+ self.request_render("portal_monitoring_need_gateway");
1697
+ }
1698
+ }
1699
+ "connect portal" | "portal" => {
1700
+ let relink = self.gateway_session_linked();
1701
+ self.state.connection_portal.reset();
1702
+ self.state
1703
+ .navigation
1704
+ .navigate_to_base(Screen::ConnectionPortal);
1705
+ self.state
1706
+ .logs
1707
+ .push_back("[NAV] Opening Connection Portal...".into());
1708
+ if relink {
1709
+ self.request_gateway_health_check(ws_client);
1710
+ }
1711
+ self.request_immediate_render("open_connection_portal");
1712
+ }
1713
+ "setup" | "setup gateway" | "setup portal" => {
1714
+ // Navigate to portal as base screen (standalone, not overlay)
1715
+ self.state.navigation.navigate_to_base(Screen::SetupPortal);
1716
+ self.state.setup_portal.soft_open();
1717
+
1718
+ // Auto-detect Gateway options when portal opens
1719
+ if let Some(ws) = ws_client {
1720
+ self.state.setup_portal.detecting = true;
1721
+ self.state.setup_portal.detecting_since =
1722
+ Some(std::time::Instant::now());
1723
+ if let Ok(id) = ws.send_command("setup.detect", None) {
1724
+ self.state.pending_setup_detect_id = Some(id);
1725
+ self.add_log(
1726
+ "[SETUP] Detecting Gateway options...".to_string(),
1727
+ );
1728
+ } else {
1729
+ self.state.setup_portal.detecting = false;
1730
+ self.state.setup_portal.detecting_since = None;
1731
+ self.state.setup_portal.error =
1732
+ Some("Failed to start detection".to_string());
1733
+ }
1734
+ } else {
1735
+ self.state.setup_portal.error = Some(
1736
+ "CLI backend not connected — auto-detect skipped. Choose an option and press Enter."
1737
+ .into(),
1738
+ );
1739
+ }
1740
+
1741
+ self.state
1742
+ .logs
1743
+ .push_back("[NAV] Opening Setup Portal...".into());
1744
+ self.request_immediate_render("open_setup_portal");
1745
+ }
1746
+ "disconnect" => {
1747
+ // Disconnect from OS Server
1748
+ if let Some(ws) = ws_client {
1749
+ match ws.send_command("gateway.disconnect", None) {
1750
+ Ok(id) => {
1751
+ let short_id = &id[id.len().saturating_sub(8)..];
1752
+ self.state.logs.push_back(format!(
1753
+ "[GATEWAY] Disconnecting (id: {})",
1754
+ short_id
1755
+ ));
1756
+ // Mode will be updated when response is received
1757
+ }
1758
+ Err(e) => {
1759
+ self.state.logs.push_back(format!(
1760
+ "[ERROR] Failed to disconnect: {}",
1761
+ e
1762
+ ));
1763
+ }
1764
+ }
1765
+ } else {
1766
+ self.state
1767
+ .logs
1768
+ .push_back("[ERROR] WebSocket not connected".into());
1769
+ }
1770
+ }
1771
+ _ => {
1772
+ // Try to parse as WebSocket command
1773
+ if let Some(ws) = ws_client {
1774
+ // Check if command looks like a WebSocket command (contains '.')
1775
+ if cmd.contains('.') {
1776
+ // Send as WebSocket command
1777
+ match ws.send_command(&cmd, None) {
1778
+ Ok(id) => {
1779
+ let short_id = &id[id.len().saturating_sub(8)..];
1780
+ self.state
1781
+ .logs
1782
+ .push_back(format!("> {} [{}]", cmd, short_id));
1783
+ }
1784
+ Err(e) => {
1785
+ self.state.logs.push_back(format!(
1786
+ "[ERROR] Failed to send '{}': {}",
1787
+ cmd, e
1788
+ ));
1789
+ }
1790
+ }
1791
+ } else {
1792
+ // Unknown local command - provide helpful guidance
1793
+ self.state
1794
+ .logs
1795
+ .push_back(format!("> {} (unknown command)", cmd));
1796
+ self.state.logs.push_back("[HELP] Try: help, build, runs, config, agent.list, system.status".into());
1797
+ }
1798
+ } else {
1799
+ // No WebSocket connection - guide to local commands
1800
+ self.state
1801
+ .logs
1802
+ .push_back(format!("> {} (WebSocket not connected)", cmd));
1803
+ self.state.logs.push_back("[WARN] Backend not running. Start backend or use local commands.".into());
1804
+ self.state.logs.push_back(
1805
+ "[HELP] Local: help, build, runs, config, clear".into(),
1806
+ );
1807
+ }
1808
+ }
1809
+ }
1810
+ self.request_render("command_submit");
1811
+ }
1812
+ }
1813
+
1814
+ // Delete character
1815
+ KeyCode::Backspace => {
1816
+ self.state.command_input.pop();
1817
+ if self.state.command_input.is_empty() {
1818
+ self.state.command_focused = false;
1819
+ }
1820
+ // OPTIMIZATION: Immediate render for instant feedback
1821
+ self.request_immediate_render("backspace_input");
1822
+ }
1823
+
1824
+ // Clear input or close overlay/popup
1825
+ KeyCode::Esc => {
1826
+ // Priority: close popup > close overlay > clear input
1827
+ if self.state.navigation.has_popup() {
1828
+ self.pop_popup();
1829
+ } else if self.state.navigation.has_overlay() {
1830
+ self.pop_overlay();
1831
+ } else if !self.state.command_input.is_empty() {
1832
+ self.state.command_input.clear();
1833
+ self.state.command_focused = false;
1834
+ self.input_debounce = None;
1835
+ self.request_render("clear_input");
1836
+ }
1837
+ }
1838
+
1839
+ // === SCROLL: Arrow keys (only when not typing) ===
1840
+ // Calculate max valid scroll based on visible height
1841
+ KeyCode::Up if self.state.command_input.is_empty() => {
1842
+ let visible_height = 15; // Approximate visible log height (adjust based on actual panel size)
1843
+ let total_logs = self.state.logs.len();
1844
+ let max_scroll = total_logs.saturating_sub(visible_height.max(1));
1845
+
1846
+ // Clamp scroll to valid range [0, max_scroll]
1847
+ self.state.log_scroll = (self.state.log_scroll + 1).min(max_scroll);
1848
+ self.request_render("scroll_up");
1849
+ }
1850
+
1851
+ KeyCode::Down if self.state.command_input.is_empty() => {
1852
+ self.state.log_scroll = self.state.log_scroll.saturating_sub(1);
1853
+ self.request_render("scroll_down");
1854
+ }
1855
+
1856
+ KeyCode::PageUp if self.state.command_input.is_empty() => {
1857
+ let visible_height = 15;
1858
+ let total_logs = self.state.logs.len();
1859
+ let max_scroll = total_logs.saturating_sub(visible_height.max(1));
1860
+
1861
+ self.state.log_scroll = (self.state.log_scroll + 10).min(max_scroll);
1862
+ self.request_render("scroll_page_up");
1863
+ }
1864
+
1865
+ KeyCode::PageDown if self.state.command_input.is_empty() => {
1866
+ self.state.log_scroll = self.state.log_scroll.saturating_sub(10);
1867
+ self.request_render("scroll_page_down");
1868
+ }
1869
+
1870
+ _ => {}
1871
+ }
1872
+
1873
+ Ok(false)
1874
+ }
1875
+
1876
+ pub fn update(&mut self, message: Message) {
1877
+ match message {
1878
+ Message::Log { message, ts } => {
1879
+ let log_line = format!("[{}] {}", ts, message);
1880
+ self.state.logs.push_back(log_line);
1881
+ while self.state.logs.len() > MAX_OPERATION_LOG_LINES {
1882
+ self.state.logs.pop_front();
1883
+ }
1884
+ }
1885
+ Message::Posture { status } => {
1886
+ self.state.posture_status = status;
1887
+ }
1888
+ Message::Metrics { cpu, mem } => {
1889
+ self.state.cpu = cpu;
1890
+ self.state.mem = mem;
1891
+ }
1892
+ Message::Network { status } => {
1893
+ self.state.network_status = status;
1894
+ }
1895
+ Message::Capabilities { items } => {
1896
+ self.state.capabilities = items;
1897
+ }
1898
+ }
1899
+ }
1900
+
1901
+ pub fn render(&mut self, f: &mut Frame) {
1902
+ // Use new navigation system - render based on current screen
1903
+ let current = self.state.navigation.current_screen();
1904
+
1905
+ // DEBUG: Log render decisions
1906
+ #[cfg(debug_assertions)]
1907
+ {
1908
+ eprintln!("[RENDER] Current screen: {:?}", current);
1909
+ eprintln!(
1910
+ "[RENDER] Base screen: {:?}",
1911
+ self.state.navigation.base_screen
1912
+ );
1913
+ eprintln!(
1914
+ "[RENDER] Overlay stack: {:?}",
1915
+ self.state.navigation.overlay_stack
1916
+ );
1917
+ }
1918
+
1919
+ match current {
1920
+ Screen::Boot => {
1921
+ use crate::ui::boot;
1922
+ boot::render_boot(f, f.size(), &self.state);
1923
+ }
1924
+ Screen::Main => {
1925
+ #[cfg(debug_assertions)]
1926
+ eprintln!("[RENDER] Rendering Main screen (layout)");
1927
+ use crate::ui::layout;
1928
+ layout::render(f, &self.state);
1929
+ }
1930
+ Screen::AgentBuilder => {
1931
+ use crate::ui::agent_builder;
1932
+ agent_builder::render(f, &self.state);
1933
+ }
1934
+ Screen::RunManager => {
1935
+ use crate::ui::run_manager;
1936
+ run_manager::render(f, &self.state);
1937
+ }
1938
+ Screen::SentinelConfig => {
1939
+ use crate::ui::sentinel_config;
1940
+ sentinel_config::render(f, &self.state);
1941
+ }
1942
+ Screen::ShieldDashboard => {
1943
+ use crate::ui::shield_dashboard;
1944
+ shield_dashboard::render(f, &self.state);
1945
+ }
1946
+ Screen::Settings => {
1947
+ use crate::ui::settings;
1948
+ settings::render(f, &self.state);
1949
+ }
1950
+ Screen::AgentList => {
1951
+ use crate::ui::agent_list;
1952
+ agent_list::render(f, &self.state);
1953
+ }
1954
+ Screen::ConnectionPortal => {
1955
+ #[cfg(debug_assertions)]
1956
+ eprintln!("[RENDER] Rendering Connection Portal (standalone)");
1957
+ // Render Connection Portal as standalone screen (not overlay)
1958
+ // This eliminates double-rendering and improves performance
1959
+ use crate::ui::connection_portal;
1960
+ connection_portal::render(f, &self.state);
1961
+ }
1962
+ Screen::SetupPortal => {
1963
+ #[cfg(debug_assertions)]
1964
+ eprintln!("[RENDER] Rendering Setup Portal (standalone)");
1965
+ // Render Setup Portal as standalone screen (not overlay)
1966
+ // This eliminates double-rendering and improves performance
1967
+ use crate::ui::setup_portal;
1968
+ setup_portal::render(f, &mut self.state);
1969
+ }
1970
+ Screen::PortalMonitoring => {
1971
+ use crate::ui::portal_monitoring;
1972
+ portal_monitoring::render(f, &mut self.state);
1973
+ }
1974
+ Screen::Confirmation { message, action } => {
1975
+ // TODO: Implement confirmation popup
1976
+ // For now, just render the base screen
1977
+ use crate::ui::layout;
1978
+ layout::render(f, &self.state);
1979
+ let _ = (message, action); // Suppress warnings
1980
+ }
1981
+ Screen::Alert { message } => {
1982
+ // TODO: Implement alert popup
1983
+ // For now, just render the base screen
1984
+ use crate::ui::layout;
1985
+ layout::render(f, &self.state);
1986
+ let _ = message; // Suppress warning
1987
+ }
1988
+ }
1989
+ }
1990
+
1991
+ /// Add a log message
1992
+ pub fn add_log(&mut self, message: String) {
1993
+ self.state.logs.push_back(message);
1994
+ while self.state.logs.len() > MAX_OPERATION_LOG_LINES {
1995
+ self.state.logs.pop_front();
1996
+ }
1997
+ self.state.log_write_count += 1;
1998
+ }
1999
+
2000
+ // ============================================================
2001
+ // NAVIGATION METHODS (NEW - Step 4.1)
2002
+ // ============================================================
2003
+
2004
+ /// Navigate to a base screen (Boot or Main)
2005
+ pub fn navigate_to(&mut self, screen: Screen) {
2006
+ if screen.is_base() {
2007
+ self.state.navigation.navigate_to_base(screen.clone());
2008
+
2009
+ // Update legacy mode field for backward compatibility
2010
+ self.state.mode = match screen {
2011
+ Screen::Boot => AppMode::Boot,
2012
+ Screen::Main => AppMode::Main,
2013
+ _ => self.state.mode.clone(),
2014
+ };
2015
+
2016
+ self.request_render("navigate_to");
2017
+ }
2018
+ }
2019
+
2020
+ /// Push an overlay screen (Agent Builder, Run Manager, Settings)
2021
+ pub fn push_overlay(&mut self, screen: Screen) {
2022
+ if screen.is_overlay() {
2023
+ self.state.navigation.push_overlay(screen);
2024
+ self.request_render("push_overlay");
2025
+ }
2026
+ }
2027
+
2028
+ /// Pop the current overlay and return to previous screen
2029
+ pub fn pop_overlay(&mut self) -> Option<Screen> {
2030
+ let result = self.state.navigation.pop_overlay();
2031
+ if result.is_some() {
2032
+ self.request_render("pop_overlay");
2033
+ }
2034
+ result
2035
+ }
2036
+
2037
+ /// Push a popup screen (Confirmation, Alert, Help)
2038
+ pub fn push_popup(&mut self, screen: Screen) {
2039
+ if screen.is_popup() {
2040
+ self.state.navigation.push_popup(screen);
2041
+ self.request_render("push_popup");
2042
+ }
2043
+ }
2044
+
2045
+ /// Pop the current popup
2046
+ pub fn pop_popup(&mut self) -> Option<Screen> {
2047
+ let result = self.state.navigation.pop_popup();
2048
+ if result.is_some() {
2049
+ self.request_render("pop_popup");
2050
+ }
2051
+ result
2052
+ }
2053
+
2054
+ /// Close all overlays and popups (return to base screen)
2055
+ #[allow(dead_code)]
2056
+ pub fn close_all_overlays(&mut self) {
2057
+ self.state.navigation.close_all();
2058
+ self.request_render("close_all");
2059
+ }
2060
+
2061
+ /// Get the currently visible screen
2062
+ #[allow(dead_code)]
2063
+ pub fn current_screen(&self) -> &Screen {
2064
+ self.state.navigation.current_screen()
2065
+ }
2066
+
2067
+ /// Check if we're on the base screen (no overlays/popups)
2068
+ #[allow(dead_code)]
2069
+ pub fn is_on_base_screen(&self) -> bool {
2070
+ self.state.navigation.is_on_base()
2071
+ }
2072
+
2073
+ // ============================================================
2074
+ // AGENT BUILDER INPUT HANDLING (Step 4.5)
2075
+ // ============================================================
2076
+
2077
+ /// Handle input when Agent Builder screen is active
2078
+ fn handle_agent_builder_input(
2079
+ &mut self,
2080
+ key: KeyEvent,
2081
+ ws_client: Option<&WebSocketClient>,
2082
+ ) -> anyhow::Result<bool> {
2083
+ use crossterm::event::KeyModifiers;
2084
+
2085
+ // Ctrl+C/Q to exit
2086
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
2087
+ match key.code {
2088
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
2089
+ _ => {}
2090
+ }
2091
+ }
2092
+
2093
+ match key.code {
2094
+ // Character input - route to appropriate field
2095
+ KeyCode::Char(c) => {
2096
+ self.handle_agent_builder_char_input(c);
2097
+ self.request_render("agent_builder_input");
2098
+ }
2099
+ // Backspace - delete character from current field
2100
+ KeyCode::Backspace => {
2101
+ self.handle_agent_builder_backspace();
2102
+ self.request_render("agent_builder_backspace");
2103
+ }
2104
+ // Left Arrow - go back to previous step
2105
+ KeyCode::Left => {
2106
+ if self.state.agent_builder.current_step > 1 {
2107
+ self.state.agent_builder.prev_step();
2108
+ self.add_log("Agent Builder: Moved to previous step".to_string());
2109
+ self.request_render("agent_builder_prev_step");
2110
+ }
2111
+ }
2112
+ // Right Arrow - move to next step (same as Enter)
2113
+ KeyCode::Right => {
2114
+ if self.state.agent_builder.current_step == 6 {
2115
+ // Final step - create agent
2116
+ self.create_agent_from_builder(ws_client);
2117
+ } else {
2118
+ // Move to next step
2119
+ if self.state.agent_builder.next_step() {
2120
+ self.request_render("agent_builder_next");
2121
+ } else {
2122
+ // Validation failed - errors are in state
2123
+ self.request_render("agent_builder_validation");
2124
+ }
2125
+ }
2126
+ }
2127
+ // Tab - move to next field (within step)
2128
+ KeyCode::Tab => {
2129
+ let shift = key.modifiers.contains(KeyModifiers::SHIFT);
2130
+ if shift {
2131
+ self.state.agent_builder.prev_field();
2132
+ } else {
2133
+ self.state.agent_builder.next_field();
2134
+ }
2135
+ self.request_render("agent_builder_field_nav");
2136
+ }
2137
+ // Enter - move to next step
2138
+ KeyCode::Enter => {
2139
+ if self.state.agent_builder.current_step == 6 {
2140
+ // Final step - create agent
2141
+ self.create_agent_from_builder(ws_client);
2142
+ } else {
2143
+ // Move to next step
2144
+ if self.state.agent_builder.next_step() {
2145
+ self.request_render("agent_builder_next");
2146
+ } else {
2147
+ // Validation failed - errors are in state
2148
+ self.request_render("agent_builder_validation");
2149
+ }
2150
+ }
2151
+ }
2152
+ // Esc - cancel and return to main
2153
+ KeyCode::Esc => {
2154
+ self.pop_overlay();
2155
+ // Reset wizard state
2156
+ self.state.agent_builder = AgentBuilderState::default();
2157
+ }
2158
+ // Arrow keys - navigate fields
2159
+ KeyCode::Up => {
2160
+ self.state.agent_builder.prev_field();
2161
+ self.request_render("agent_builder_field_nav");
2162
+ }
2163
+ KeyCode::Down => {
2164
+ self.state.agent_builder.next_field();
2165
+ self.request_render("agent_builder_field_nav");
2166
+ }
2167
+ _ => {}
2168
+ }
2169
+
2170
+ Ok(false)
2171
+ }
2172
+
2173
+ /// Handle character input for Agent Builder fields
2174
+ fn handle_agent_builder_char_input(&mut self, c: char) {
2175
+ let step = self.state.agent_builder.current_step;
2176
+ let field = self.state.agent_builder.focused_field;
2177
+
2178
+ match step {
2179
+ 1 => {
2180
+ // Step 1: Basic Info
2181
+ match field {
2182
+ 0 => self.state.agent_builder.name.push(c),
2183
+ 1 => self.state.agent_builder.description.push(c),
2184
+ _ => {}
2185
+ }
2186
+ }
2187
+ 2 => {
2188
+ // Step 2: Model Selection
2189
+ match field {
2190
+ 0 => self.state.agent_builder.provider.push(c),
2191
+ 1 => self.state.agent_builder.model.push(c),
2192
+ 2 => self.state.agent_builder.local_provider.push(c),
2193
+ 3 => self.state.agent_builder.local_model.push(c),
2194
+ 4 => self.state.agent_builder.local_url.push(c),
2195
+ _ => {}
2196
+ }
2197
+ }
2198
+ 3 => {
2199
+ // Step 3: System Prompt
2200
+ self.state.agent_builder.system_prompt.push(c);
2201
+ }
2202
+ 4 => {
2203
+ // Step 4: Parameters (numeric input)
2204
+ match field {
2205
+ 0 => {
2206
+ // Temperature - allow digits and decimal point
2207
+ if c.is_ascii_digit() || c == '.' {
2208
+ let mut temp_str = format!("{}", self.state.agent_builder.temperature);
2209
+ temp_str.push(c);
2210
+ if let Ok(val) = temp_str.parse::<f32>() {
2211
+ self.state.agent_builder.temperature = val;
2212
+ }
2213
+ }
2214
+ }
2215
+ 1 => {
2216
+ // Max tokens - digits only
2217
+ if c.is_ascii_digit() {
2218
+ let mut tokens_str = format!("{}", self.state.agent_builder.max_tokens);
2219
+ tokens_str.push(c);
2220
+ if let Ok(val) = tokens_str.parse::<u32>() {
2221
+ self.state.agent_builder.max_tokens = val;
2222
+ }
2223
+ }
2224
+ }
2225
+ _ => {}
2226
+ }
2227
+ }
2228
+ 5 => {
2229
+ // Step 5: Tools - toggle with space
2230
+ if c == ' ' {
2231
+ let tools = &self.state.agent_builder.available_tools;
2232
+ if field < tools.len() {
2233
+ let tool = tools[field].clone();
2234
+ if self.state.agent_builder.selected_tools.contains(&tool) {
2235
+ self.state
2236
+ .agent_builder
2237
+ .selected_tools
2238
+ .retain(|t| t != &tool);
2239
+ } else {
2240
+ self.state.agent_builder.selected_tools.push(tool);
2241
+ }
2242
+ }
2243
+ }
2244
+ }
2245
+ _ => {}
2246
+ }
2247
+ }
2248
+
2249
+ /// Handle backspace for Agent Builder fields
2250
+ fn handle_agent_builder_backspace(&mut self) {
2251
+ let step = self.state.agent_builder.current_step;
2252
+ let field = self.state.agent_builder.focused_field;
2253
+
2254
+ match step {
2255
+ 1 => match field {
2256
+ 0 => {
2257
+ self.state.agent_builder.name.pop();
2258
+ }
2259
+ 1 => {
2260
+ self.state.agent_builder.description.pop();
2261
+ }
2262
+ _ => {}
2263
+ },
2264
+ 2 => match field {
2265
+ 0 => {
2266
+ self.state.agent_builder.provider.pop();
2267
+ }
2268
+ 1 => {
2269
+ self.state.agent_builder.model.pop();
2270
+ }
2271
+ 2 => {
2272
+ self.state.agent_builder.local_provider.pop();
2273
+ }
2274
+ 3 => {
2275
+ self.state.agent_builder.local_model.pop();
2276
+ }
2277
+ 4 => {
2278
+ self.state.agent_builder.local_url.pop();
2279
+ }
2280
+ _ => {}
2281
+ },
2282
+ 3 => {
2283
+ self.state.agent_builder.system_prompt.pop();
2284
+ }
2285
+ 4 => {
2286
+ // Parameters - backspace on numeric fields
2287
+ match field {
2288
+ 0 => {
2289
+ let mut temp_str = format!("{}", self.state.agent_builder.temperature);
2290
+ temp_str.pop();
2291
+ if let Ok(val) = temp_str.parse::<f32>() {
2292
+ self.state.agent_builder.temperature = val;
2293
+ } else if temp_str.is_empty() {
2294
+ self.state.agent_builder.temperature = 0.0;
2295
+ }
2296
+ }
2297
+ 1 => {
2298
+ let mut tokens_str = format!("{}", self.state.agent_builder.max_tokens);
2299
+ tokens_str.pop();
2300
+ if let Ok(val) = tokens_str.parse::<u32>() {
2301
+ self.state.agent_builder.max_tokens = val;
2302
+ } else if tokens_str.is_empty() {
2303
+ self.state.agent_builder.max_tokens = 0;
2304
+ }
2305
+ }
2306
+ _ => {}
2307
+ }
2308
+ }
2309
+ _ => {}
2310
+ }
2311
+ }
2312
+
2313
+ /// Create agent from builder state and send to backend
2314
+ fn create_agent_from_builder(&mut self, ws_client: Option<&WebSocketClient>) {
2315
+ // Validate one final time
2316
+ if !self.state.agent_builder.validate_step() {
2317
+ self.add_log(
2318
+ "[ERROR] Agent validation failed. Please fix errors and try again.".to_string(),
2319
+ );
2320
+ // Clone errors to avoid borrow checker issues
2321
+ let errors = self.state.agent_builder.validation_errors.clone();
2322
+ for error in &errors {
2323
+ self.add_log(format!(" - {}", error));
2324
+ }
2325
+ return;
2326
+ }
2327
+
2328
+ // Clone name before borrowing mutably
2329
+ let agent_name = self.state.agent_builder.name.clone();
2330
+
2331
+ // Validate name is not empty
2332
+ if agent_name.trim().is_empty() {
2333
+ self.add_log("[ERROR] Agent name cannot be empty".to_string());
2334
+ return;
2335
+ }
2336
+
2337
+ // Build agent data
2338
+ let builder = &self.state.agent_builder;
2339
+ let agent_data = serde_json::json!({
2340
+ "name": builder.name,
2341
+ "description": builder.description,
2342
+ "baseAgent": if builder.use_local_model {
2343
+ builder.local_model.clone()
2344
+ } else {
2345
+ builder.model.clone()
2346
+ },
2347
+ "systemPrompt": builder.system_prompt,
2348
+ "temperature": builder.temperature,
2349
+ "maxTokens": builder.max_tokens,
2350
+ "useLocalModel": builder.use_local_model,
2351
+ "localModelProvider": if builder.use_local_model {
2352
+ Some(builder.local_provider.clone())
2353
+ } else {
2354
+ None
2355
+ },
2356
+ "localModelName": if builder.use_local_model {
2357
+ Some(builder.local_model.clone())
2358
+ } else {
2359
+ None
2360
+ },
2361
+ "localModelUrl": if builder.use_local_model {
2362
+ Some(builder.local_url.clone())
2363
+ } else {
2364
+ None
2365
+ },
2366
+ "topP": builder.top_p,
2367
+ "frequencyPenalty": builder.frequency_penalty,
2368
+ "presencePenalty": builder.presence_penalty,
2369
+ "tools": builder.selected_tools,
2370
+ });
2371
+
2372
+ // Log the creation attempt
2373
+ self.add_log(format!("[AGENT] Creating agent '{}'...", agent_name));
2374
+
2375
+ // Send agent.create command via WebSocket
2376
+ if let Some(ws) = ws_client {
2377
+ match ws.send_command("agent.create", Some(agent_data)) {
2378
+ Ok(id) => {
2379
+ let short_id = &id[id.len().saturating_sub(8)..];
2380
+ self.add_log(format!("[AGENT] Sent agent.create (id: {})", short_id));
2381
+ // Store command ID for response tracking (Step 5.1)
2382
+ self.state.pending_agent_create_id = Some(id);
2383
+ }
2384
+ Err(e) => {
2385
+ self.add_log(format!("[ERROR] Failed to create agent: {}", e));
2386
+ return;
2387
+ }
2388
+ }
2389
+ } else {
2390
+ self.add_log("[ERROR] WebSocket not connected. Cannot create agent.".to_string());
2391
+ self.add_log("[HELP] Start the backend server and reconnect.".to_string());
2392
+ return;
2393
+ }
2394
+
2395
+ // Close the builder
2396
+ self.pop_overlay();
2397
+
2398
+ // Reset wizard state
2399
+ self.state.agent_builder = AgentBuilderState::default();
2400
+
2401
+ // Suppress unused variable warning
2402
+ let _ = agent_data;
2403
+ }
2404
+
2405
+ // ============================================================
2406
+ // RUN MANAGER INPUT HANDLING (Step 4.7)
2407
+ // ============================================================
2408
+
2409
+ /// Handle input when Run Manager screen is active
2410
+ fn handle_run_manager_input(
2411
+ &mut self,
2412
+ key: KeyEvent,
2413
+ ws_client: Option<&WebSocketClient>,
2414
+ ) -> anyhow::Result<bool> {
2415
+ use crossterm::event::KeyModifiers;
2416
+
2417
+ // Ctrl+C/Q to exit
2418
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
2419
+ match key.code {
2420
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
2421
+ _ => return Ok(false),
2422
+ }
2423
+ }
2424
+
2425
+ match key.code {
2426
+ // Navigation — list only; detail view locks to pinned run
2427
+ KeyCode::Up => {
2428
+ if self.state.run_manager.is_detail_view() {
2429
+ self.state.run_manager.detail_log_scroll_up();
2430
+ } else {
2431
+ self.state.run_manager.select_previous();
2432
+ }
2433
+ self.request_render("run_manager_up");
2434
+ }
2435
+ KeyCode::Down => {
2436
+ if self.state.run_manager.is_detail_view() {
2437
+ self.state.run_manager.detail_log_scroll_down();
2438
+ } else {
2439
+ self.state.run_manager.select_next();
2440
+ }
2441
+ self.request_render("run_manager_down");
2442
+ }
2443
+
2444
+ // Filter (list view only)
2445
+ KeyCode::Char('f') | KeyCode::Char('F') => {
2446
+ if !self.state.run_manager.is_detail_view() {
2447
+ self.state.run_manager.next_filter();
2448
+ self.request_render("run_manager_filter");
2449
+ }
2450
+ }
2451
+
2452
+ // Sort (list view only)
2453
+ KeyCode::Char('s') | KeyCode::Char('S') => {
2454
+ if !self.state.run_manager.is_detail_view() {
2455
+ self.state.run_manager.next_sort();
2456
+ self.request_render("run_manager_sort");
2457
+ }
2458
+ }
2459
+
2460
+ // Refresh (list or pinned detail)
2461
+ KeyCode::Char('r') | KeyCode::Char('R') => {
2462
+ if self.state.operation_mode == OperationMode::Connected {
2463
+ if let Some(ws) = ws_client {
2464
+ if self.state.run_manager.is_detail_view() {
2465
+ if let Some(run) = self.state.run_manager.detail_run() {
2466
+ let run_id = run.id.clone();
2467
+ self.begin_run_get_request(ws, &run_id);
2468
+ self.add_log("[RUN] Refreshing run detail...".to_string());
2469
+ }
2470
+ } else {
2471
+ self.begin_run_list_request(ws, false);
2472
+ self.add_log("[RUN] Refreshing run list from OS Server...".to_string());
2473
+ }
2474
+ } else {
2475
+ self.add_log("[RUN] WebSocket not connected".to_string());
2476
+ }
2477
+ } else {
2478
+ self.add_log("[RUN] Connect to OS Server to refresh runs".to_string());
2479
+ }
2480
+ self.request_render("run_manager_refresh");
2481
+ }
2482
+
2483
+ // Toggle live auto-refresh (poll every ~3s)
2484
+ KeyCode::Char('a') | KeyCode::Char('A') => {
2485
+ self.state.run_manager.auto_refresh_enabled =
2486
+ !self.state.run_manager.auto_refresh_enabled;
2487
+ let status = if self.state.run_manager.auto_refresh_enabled {
2488
+ "on"
2489
+ } else {
2490
+ "off"
2491
+ };
2492
+ self.add_log(format!("[RUN] Live auto-refresh {}", status));
2493
+ self.request_render("run_manager_live_toggle");
2494
+ }
2495
+
2496
+ // View details / Close detail view
2497
+ KeyCode::Enter => {
2498
+ if self.state.run_manager.is_detail_view() {
2499
+ self.state.run_manager.close_detail_view();
2500
+ self.add_log("[RUN] Closed detail view".to_string());
2501
+ } else if let Some(run_id) = self.state.run_manager.open_detail_view() {
2502
+ if let Some(run) = self.state.run_manager.detail_run() {
2503
+ self.add_log(format!("[RUN] Viewing run: {}", run.name));
2504
+ }
2505
+ if self.state.operation_mode == OperationMode::Connected {
2506
+ if let Some(ws) = ws_client {
2507
+ self.begin_run_get_request(ws, &run_id);
2508
+ }
2509
+ }
2510
+ } else {
2511
+ self.add_log("[RUN] No run selected".to_string());
2512
+ }
2513
+ self.request_render("run_manager_view");
2514
+ }
2515
+
2516
+ // Cancel run (list selection or pinned detail run)
2517
+ KeyCode::Char('c') | KeyCode::Char('C') => {
2518
+ let run = if self.state.run_manager.is_detail_view() {
2519
+ self.state.run_manager.detail_run()
2520
+ } else {
2521
+ self.state.run_manager.selected_run()
2522
+ };
2523
+ if let Some(run) = run {
2524
+ if matches!(
2525
+ run.status,
2526
+ crate::ui::run_manager::RunStatus::Running
2527
+ | crate::ui::run_manager::RunStatus::Pending
2528
+ ) {
2529
+ if self.state.operation_mode == OperationMode::Connected {
2530
+ if let Some(ws) = ws_client {
2531
+ let data = serde_json::json!({ "runId": run.id.clone() });
2532
+ if let Ok(id) = ws.send_command("run.cancel", Some(data)) {
2533
+ self.state.pending_run_cancel_id = Some(id);
2534
+ self.add_log(format!("[RUN] Cancelling run: {}", run.name));
2535
+ }
2536
+ } else {
2537
+ self.add_log("[RUN] WebSocket not connected".to_string());
2538
+ }
2539
+ } else {
2540
+ self.add_log("[RUN] Connect to OS Server to cancel runs".to_string());
2541
+ }
2542
+ } else {
2543
+ self.add_log("[RUN] Cannot cancel completed/failed run".to_string());
2544
+ }
2545
+ }
2546
+ self.request_render("run_manager_cancel");
2547
+ }
2548
+
2549
+ // Delete run
2550
+ KeyCode::Char('d') | KeyCode::Char('D') => {
2551
+ self.add_log("[RUN] Delete run is not supported by the OS Server API (use cancel for active runs).".to_string());
2552
+ self.request_render("run_manager_delete");
2553
+ }
2554
+
2555
+ // Close detail view or Run Manager
2556
+ KeyCode::Esc => {
2557
+ if self.state.run_manager.return_to_overlay == Some(Screen::ShieldDashboard) {
2558
+ self.state.run_manager.return_to_overlay = None;
2559
+ self.state.run_manager.close_detail_view();
2560
+ self.pop_overlay();
2561
+ if self.state.navigation.current_screen() != &Screen::ShieldDashboard {
2562
+ self.push_overlay(Screen::ShieldDashboard);
2563
+ }
2564
+ self.add_log("[RUN] Returned to Shield".to_string());
2565
+ self.request_render("run_manager_return_shield");
2566
+ } else if self.state.run_manager.is_detail_view() {
2567
+ self.state.run_manager.close_detail_view();
2568
+ self.add_log("[RUN] Closed detail view".to_string());
2569
+ self.request_render("run_manager_close_detail");
2570
+ } else {
2571
+ self.pop_overlay();
2572
+ }
2573
+ }
2574
+
2575
+ _ => {}
2576
+ }
2577
+
2578
+ Ok(false)
2579
+ }
2580
+
2581
+ pub fn sentinel_scroll_custom_value(&mut self, increase: bool) {
2582
+ self.sentinel_adjust_custom_field(increase);
2583
+ }
2584
+
2585
+ fn sentinel_adjust_custom_field(&mut self, increase: bool) {
2586
+ use crate::ui::sentinel_config::SentinelViewMode;
2587
+ if self.state.sentinel_config.view_mode != SentinelViewMode::Custom {
2588
+ return;
2589
+ }
2590
+ self.state.sentinel_config.adjust_custom_field(increase);
2591
+ // Clear stale apply banner so edits are obvious; shortcuts return in footer
2592
+ self.state.sentinel_config.status_message = None;
2593
+ self.request_immediate_render(if increase {
2594
+ "sentinel_inc"
2595
+ } else {
2596
+ "sentinel_dec"
2597
+ });
2598
+ }
2599
+
2600
+ /// Handle input when Sentinel Config screen is active
2601
+ fn handle_sentinel_config_input(
2602
+ &mut self,
2603
+ key: KeyEvent,
2604
+ ws_client: Option<&WebSocketClient>,
2605
+ ) -> anyhow::Result<bool> {
2606
+ use crate::ui::sentinel_config::SentinelViewMode;
2607
+ use crossterm::event::KeyModifiers;
2608
+
2609
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
2610
+ match key.code {
2611
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
2612
+ _ => return Ok(false),
2613
+ }
2614
+ }
2615
+
2616
+ match key.code {
2617
+ KeyCode::Tab => {
2618
+ self.state.sentinel_config.cycle_view_mode();
2619
+ self.request_immediate_render("sentinel_tab");
2620
+ }
2621
+ KeyCode::Up => {
2622
+ self.state.sentinel_config.select_previous();
2623
+ self.request_immediate_render("sentinel_up");
2624
+ }
2625
+ KeyCode::Down => {
2626
+ self.state.sentinel_config.select_next();
2627
+ self.request_immediate_render("sentinel_down");
2628
+ }
2629
+ KeyCode::Left => {
2630
+ if self.state.sentinel_config.view_mode == SentinelViewMode::Custom {
2631
+ self.sentinel_adjust_custom_field(false);
2632
+ }
2633
+ }
2634
+ KeyCode::Right => {
2635
+ if self.state.sentinel_config.view_mode == SentinelViewMode::Custom {
2636
+ self.sentinel_adjust_custom_field(true);
2637
+ }
2638
+ }
2639
+ KeyCode::Char('+') | KeyCode::Char('=') => {
2640
+ if self.state.sentinel_config.view_mode == SentinelViewMode::Custom {
2641
+ self.sentinel_adjust_custom_field(true);
2642
+ }
2643
+ }
2644
+ KeyCode::Char('-') | KeyCode::Char('_') => {
2645
+ if self.state.sentinel_config.view_mode == SentinelViewMode::Custom {
2646
+ self.sentinel_adjust_custom_field(false);
2647
+ }
2648
+ }
2649
+ KeyCode::Char(' ') => {
2650
+ if self.state.sentinel_config.view_mode == SentinelViewMode::Custom {
2651
+ use crate::ui::sentinel_config::CustomField;
2652
+ if self.state.sentinel_config.custom_field == CustomField::Enabled {
2653
+ self.sentinel_adjust_custom_field(true);
2654
+ }
2655
+ }
2656
+ }
2657
+ KeyCode::Char('r') | KeyCode::Char('R') => {
2658
+ if self.state.operation_mode == OperationMode::Connected {
2659
+ if let Some(ws) = ws_client {
2660
+ self.begin_sentinel_load_request(ws);
2661
+ }
2662
+ }
2663
+ self.request_render("sentinel_refresh");
2664
+ }
2665
+ KeyCode::Enter => {
2666
+ if self.state.operation_mode != OperationMode::Connected {
2667
+ self.state.sentinel_config.error =
2668
+ Some("Connect to OS Server first.".to_string());
2669
+ } else if let Some(ws) = ws_client {
2670
+ // Apply custom configuration
2671
+ let draft = self.state.sentinel_config.custom_draft.clone();
2672
+ self.begin_sentinel_apply_custom_request(ws, &draft);
2673
+ self.add_log("[SENTINEL] Applying custom limits...".to_string());
2674
+ }
2675
+ self.request_render("sentinel_apply");
2676
+ }
2677
+ KeyCode::Esc => {
2678
+ self.pop_overlay();
2679
+ self.request_render("sentinel_close");
2680
+ }
2681
+ _ => {}
2682
+ }
2683
+
2684
+ Ok(false)
2685
+ }
2686
+
2687
+ fn handle_shield_dashboard_input(
2688
+ &mut self,
2689
+ key: KeyEvent,
2690
+ ws_client: Option<&WebSocketClient>,
2691
+ ) -> anyhow::Result<bool> {
2692
+ use crate::ui::shield_dashboard::ShieldTab;
2693
+ use crossterm::event::KeyModifiers;
2694
+
2695
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
2696
+ match key.code {
2697
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
2698
+ _ => return Ok(false),
2699
+ }
2700
+ }
2701
+
2702
+ match key.code {
2703
+ KeyCode::Tab => {
2704
+ self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.next();
2705
+ self.request_render("shield_tab");
2706
+ }
2707
+ KeyCode::BackTab => {
2708
+ self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.prev();
2709
+ self.request_render("shield_tab_back");
2710
+ }
2711
+ KeyCode::Left => {
2712
+ self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.prev();
2713
+ self.request_render("shield_tab_left");
2714
+ }
2715
+ KeyCode::Right => {
2716
+ self.state.shield_dashboard.tab = self.state.shield_dashboard.tab.next();
2717
+ self.request_render("shield_tab_right");
2718
+ }
2719
+ KeyCode::Up => {
2720
+ if self.state.shield_dashboard.tab == ShieldTab::Activity
2721
+ && self.state.shield_dashboard.selected_activity_index > 0
2722
+ {
2723
+ self.state.shield_dashboard.selected_activity_index -= 1;
2724
+ self.request_render("shield_activity_up");
2725
+ } else if self.state.shield_dashboard.tab == ShieldTab::Enforcement
2726
+ && self.state.shield_dashboard.selected_block_index > 0
2727
+ {
2728
+ self.state.shield_dashboard.selected_block_index -= 1;
2729
+ self.request_render("shield_block_up");
2730
+ }
2731
+ }
2732
+ KeyCode::Down => {
2733
+ if self.state.shield_dashboard.tab == ShieldTab::Activity {
2734
+ let max = self.state.shield_dashboard.activity_events.len();
2735
+ if max > 0 && self.state.shield_dashboard.selected_activity_index + 1 < max {
2736
+ self.state.shield_dashboard.selected_activity_index += 1;
2737
+ self.request_render("shield_activity_down");
2738
+ }
2739
+ } else if self.state.shield_dashboard.tab == ShieldTab::Enforcement {
2740
+ let max = self.state.shield_dashboard.recent_blocks.len();
2741
+ if max > 0 && self.state.shield_dashboard.selected_block_index + 1 < max {
2742
+ self.state.shield_dashboard.selected_block_index += 1;
2743
+ self.request_render("shield_block_down");
2744
+ }
2745
+ }
2746
+ }
2747
+ KeyCode::Enter => {
2748
+ if self.state.shield_dashboard.tab == ShieldTab::Activity {
2749
+ let run_id = self
2750
+ .state
2751
+ .shield_dashboard
2752
+ .activity_events
2753
+ .get(self.state.shield_dashboard.selected_activity_index)
2754
+ .and_then(|row| row.run_id.clone());
2755
+ if let Some(run_id) = run_id {
2756
+ self.open_run_from_shield_block(ws_client, &run_id);
2757
+ }
2758
+ } else if self.state.shield_dashboard.tab == ShieldTab::Enforcement {
2759
+ if let Some(row) = self
2760
+ .state
2761
+ .shield_dashboard
2762
+ .recent_blocks
2763
+ .get(self.state.shield_dashboard.selected_block_index)
2764
+ {
2765
+ let run_id = row.id.clone();
2766
+ self.open_run_from_shield_block(ws_client, &run_id);
2767
+ }
2768
+ }
2769
+ }
2770
+ KeyCode::Char('r') | KeyCode::Char('R') => {
2771
+ if self.state.operation_mode == OperationMode::Connected {
2772
+ if let Some(ws) = ws_client {
2773
+ self.begin_shield_load_request(ws, true);
2774
+ }
2775
+ } else {
2776
+ self.state.shield_dashboard.fail_loading(
2777
+ "Connect to OS Server first (connect portal).".to_string(),
2778
+ );
2779
+ }
2780
+ self.request_render("shield_refresh");
2781
+ }
2782
+ KeyCode::Char('o') | KeyCode::Char('O') => {
2783
+ self.push_overlay(Screen::RunManager);
2784
+ if let Some(ws) = ws_client {
2785
+ self.begin_run_list_request(ws, false);
2786
+ }
2787
+ self.add_log("[SHIELD] Opened Run Manager".to_string());
2788
+ self.request_render("shield_open_runs");
2789
+ }
2790
+ KeyCode::Char('p') | KeyCode::Char('P') => {
2791
+ if self.state.operation_mode == OperationMode::Connected {
2792
+ self.state.navigation.navigate_to_base(Screen::PortalMonitoring);
2793
+ if let Some(ws) = ws_client {
2794
+ self.begin_portal_observability_request(ws);
2795
+ }
2796
+ self.add_log("[SHIELD] Opened Portal Monitoring".to_string());
2797
+ } else {
2798
+ self.add_log("[SHIELD] Connect to OS Server first.".to_string());
2799
+ }
2800
+ self.request_render("shield_open_monitoring");
2801
+ }
2802
+ KeyCode::Char('a') | KeyCode::Char('A') => {
2803
+ self.state.shield_dashboard.auto_refresh_enabled =
2804
+ !self.state.shield_dashboard.auto_refresh_enabled;
2805
+ let status = if self.state.shield_dashboard.auto_refresh_enabled {
2806
+ "ON"
2807
+ } else {
2808
+ "OFF"
2809
+ };
2810
+ self.add_log(format!("[SHIELD] Live refresh {}", status));
2811
+ self.request_render("shield_live_toggle");
2812
+ }
2813
+ KeyCode::Esc => {
2814
+ self.pop_overlay();
2815
+ self.request_render("shield_close");
2816
+ }
2817
+ _ => {}
2818
+ }
2819
+
2820
+ Ok(false)
2821
+ }
2822
+
2823
+ // ============================================================
2824
+ // SETTINGS INPUT HANDLING (Step 4.8)
2825
+ // ============================================================
2826
+
2827
+ /// Handle input when Settings screen is active
2828
+ fn handle_settings_input(&mut self, key: KeyEvent) -> anyhow::Result<bool> {
2829
+ use crossterm::event::KeyModifiers;
2830
+
2831
+ // Ctrl+C/Q to exit
2832
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
2833
+ match key.code {
2834
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
2835
+ _ => return Ok(false),
2836
+ }
2837
+ }
2838
+
2839
+ match key.code {
2840
+ // Navigation
2841
+ KeyCode::Up => {
2842
+ self.state.settings.prev_section();
2843
+ self.request_render("settings_up");
2844
+ }
2845
+ KeyCode::Down => {
2846
+ self.state.settings.next_section();
2847
+ self.request_render("settings_down");
2848
+ }
2849
+
2850
+ // Toggle current setting
2851
+ KeyCode::Char(' ') => {
2852
+ match self.state.settings.focused_section {
2853
+ crate::ui::settings::SettingsSection::Mode => {
2854
+ self.state.settings.toggle_mode();
2855
+ self.add_log(format!(
2856
+ "[SETTINGS] Mode: {}",
2857
+ self.state.settings.mode.as_str()
2858
+ ));
2859
+ }
2860
+ crate::ui::settings::SettingsSection::AIProvider => {
2861
+ self.state.settings.next_provider();
2862
+ self.add_log(format!(
2863
+ "[SETTINGS] Provider: {}",
2864
+ self.state.settings.ai_provider
2865
+ ));
2866
+ }
2867
+ crate::ui::settings::SettingsSection::AutoUpdate => {
2868
+ self.state.settings.toggle_auto_update();
2869
+ self.add_log(format!(
2870
+ "[SETTINGS] Auto-update: {}",
2871
+ self.state.settings.auto_update
2872
+ ));
2873
+ }
2874
+ _ => {}
2875
+ }
2876
+ self.request_render("settings_toggle");
2877
+ }
2878
+
2879
+ // Save settings
2880
+ KeyCode::Enter => {
2881
+ if self.state.settings.has_changes {
2882
+ self.state.settings.save();
2883
+ self.add_log("[SETTINGS] Settings saved successfully".to_string());
2884
+ // TODO: Send config.update command via WebSocket
2885
+ self.request_render("settings_save");
2886
+ }
2887
+ }
2888
+
2889
+ // Close (with discard warning if changes)
2890
+ KeyCode::Esc => {
2891
+ if self.state.settings.has_changes {
2892
+ self.add_log("[SETTINGS] Discarding unsaved changes".to_string());
2893
+ self.state.settings.discard();
2894
+ }
2895
+ self.pop_overlay();
2896
+ }
2897
+
2898
+ _ => {}
2899
+ }
2900
+
2901
+ Ok(false)
2902
+ }
2903
+
2904
+ /// Handle input when Agent List screen is active
2905
+ fn handle_agent_list_input(
2906
+ &mut self,
2907
+ key: KeyEvent,
2908
+ ws_client: Option<&WebSocketClient>,
2909
+ ) -> anyhow::Result<bool> {
2910
+ use crossterm::event::KeyModifiers;
2911
+
2912
+ // Ctrl+C/Q to exit
2913
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
2914
+ match key.code {
2915
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
2916
+ _ => return Ok(false),
2917
+ }
2918
+ }
2919
+
2920
+ match key.code {
2921
+ // Navigation
2922
+ KeyCode::Up => {
2923
+ if self.state.agent_list.selected_index > 0 {
2924
+ self.state.agent_list.selected_index -= 1;
2925
+ }
2926
+ self.request_render("agent_list_up");
2927
+ }
2928
+ KeyCode::Down => {
2929
+ let max = self.state.agent_list.agents.len().saturating_sub(1);
2930
+ if self.state.agent_list.selected_index < max {
2931
+ self.state.agent_list.selected_index += 1;
2932
+ }
2933
+ self.request_render("agent_list_down");
2934
+ }
2935
+
2936
+ // View details / Close detail view
2937
+ KeyCode::Enter => {
2938
+ if self.state.agent_list.detail_view.is_some() {
2939
+ // Close detail view
2940
+ self.state.agent_list.detail_view = None;
2941
+ } else {
2942
+ // Open detail view for selected agent
2943
+ self.state.agent_list.detail_view = Some(self.state.agent_list.selected_index);
2944
+ }
2945
+ self.request_render("agent_list_toggle_detail");
2946
+ }
2947
+
2948
+ // Close
2949
+ KeyCode::Esc => {
2950
+ if self.state.agent_list.detail_view.is_some() {
2951
+ // Close detail popup first
2952
+ self.state.agent_list.detail_view = None;
2953
+ self.request_render("agent_list_close_detail");
2954
+ } else {
2955
+ // Close agent list
2956
+ self.pop_overlay();
2957
+ self.request_render("agent_list_close");
2958
+ }
2959
+ }
2960
+
2961
+ // Delete agent (Step 5.5)
2962
+ KeyCode::Delete | KeyCode::Char('d') | KeyCode::Char('D') => {
2963
+ // Only allow deletion in list view (not in detail view)
2964
+ if self.state.agent_list.detail_view.is_none()
2965
+ && !self.state.agent_list.agents.is_empty()
2966
+ {
2967
+ self.state.agent_delete_requested = true;
2968
+ self.request_render("agent_delete_requested");
2969
+ }
2970
+ }
2971
+
2972
+ // Step 6: queue a Gateway run (uses built-in `test` agent executor on the server)
2973
+ KeyCode::Char('x') | KeyCode::Char('X') => {
2974
+ if self.state.operation_mode != OperationMode::Connected {
2975
+ self.add_log(
2976
+ "[AGENT] Connect to OS Server first to run on the server (key x)."
2977
+ .to_string(),
2978
+ );
2979
+ } else if self.state.agent_list.agents.is_empty() {
2980
+ self.add_log("[AGENT] No agents in list.".to_string());
2981
+ } else if let Some((agent_name, agent_model)) = self
2982
+ .get_selected_agent()
2983
+ .map(|a| (a.name.clone(), a.model.clone()))
2984
+ {
2985
+ if let Some(ws) = ws_client {
2986
+ let name = format!("TUI: {}", agent_name);
2987
+ let input = serde_json::json!({
2988
+ "agent_id": "test",
2989
+ "data": {
2990
+ "prompt": format!("Quick run for local agent profile '{}' (model {}).", agent_name, agent_model)
2991
+ }
2992
+ });
2993
+ let data = serde_json::json!({ "name": name, "input": input });
2994
+ match ws.send_command("run.quick", Some(data)) {
2995
+ Ok(id) => {
2996
+ self.state.pending_run_quick_id = Some(id);
2997
+ self.add_log(format!(
2998
+ "[AGENT] Queued Gateway run for profile '{}' (x)",
2999
+ agent_name
3000
+ ));
3001
+ }
3002
+ Err(e) => self.add_log(format!("[ERROR] run.quick: {}", e)),
3003
+ }
3004
+ }
3005
+ }
3006
+ self.request_render("agent_list_quick_run");
3007
+ }
3008
+
3009
+ _ => {}
3010
+ }
3011
+
3012
+ Ok(false)
3013
+ }
3014
+
3015
+ /// Delete selected agent (Step 5.5)
3016
+ pub fn delete_selected_agent(&mut self, ws_client: &Option<crate::websocket::WebSocketClient>) {
3017
+ if self.state.agent_list.agents.is_empty() {
3018
+ return;
3019
+ }
3020
+
3021
+ let agent_name = self.state.agent_list.agents[self.state.agent_list.selected_index]
3022
+ .name
3023
+ .clone();
3024
+
3025
+ // Send agent.delete command via WebSocket
3026
+ if let Some(ws) = ws_client {
3027
+ let delete_data = serde_json::json!({
3028
+ "name": agent_name
3029
+ });
3030
+
3031
+ match ws.send_command("agent.delete", Some(delete_data)) {
3032
+ Ok(id) => {
3033
+ let short_id = &id[id.len().saturating_sub(8)..];
3034
+ self.add_log(format!(
3035
+ "[AGENT] Sent agent.delete for '{}' (id: {})",
3036
+ agent_name, short_id
3037
+ ));
3038
+ // Store command ID for response tracking
3039
+ self.state.pending_agent_delete_id = Some(id);
3040
+ }
3041
+ Err(e) => {
3042
+ self.add_log(format!("[ERROR] Failed to delete agent: {}", e));
3043
+ }
3044
+ }
3045
+ } else {
3046
+ self.add_log("[ERROR] WebSocket not connected. Cannot delete agent.".to_string());
3047
+ }
3048
+ }
3049
+
3050
+ /// Get selected agent (Step 5.6)
3051
+ pub fn get_selected_agent(&self) -> Option<&AgentInfo> {
3052
+ if self.state.agent_list.agents.is_empty() {
3053
+ return None;
3054
+ }
3055
+ self.state
3056
+ .agent_list
3057
+ .agents
3058
+ .get(self.state.agent_list.selected_index)
3059
+ }
3060
+
3061
+ /// Get selected agent name (Step 5.6)
3062
+ pub fn get_selected_agent_name(&self) -> Option<String> {
3063
+ self.get_selected_agent().map(|agent| agent.name.clone())
3064
+ }
3065
+
3066
+ /// Handle input when Connection Portal screen is active (Step 7)
3067
+ fn handle_connection_portal_input(
3068
+ &mut self,
3069
+ key: KeyEvent,
3070
+ ws_client: Option<&WebSocketClient>,
3071
+ ) -> anyhow::Result<bool> {
3072
+ use crossterm::event::KeyModifiers;
3073
+
3074
+ // Ctrl+C/Q to exit
3075
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
3076
+ match key.code {
3077
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
3078
+ _ => return Ok(false),
3079
+ }
3080
+ }
3081
+
3082
+ // Disable input if connecting
3083
+ if self.state.connection_portal.connecting {
3084
+ return Ok(false);
3085
+ }
3086
+
3087
+ match key.code {
3088
+ // ESC - Close portal and return to Main
3089
+ KeyCode::Esc => {
3090
+ // Drop stale connect attempt + UI state so the next open is not "pre-broken".
3091
+ self.state.pending_gateway_connect_id = None;
3092
+ self.state.connection_portal.finish_connecting();
3093
+ self.state.connection_portal.error = None;
3094
+ self.state.connection_portal.activity_log.clear();
3095
+ self.state.navigation.navigate_to_base(Screen::Main);
3096
+ self.add_log("[NAV] Connection Portal closed".to_string());
3097
+ self.request_immediate_render("portal_close");
3098
+ }
3099
+
3100
+ // M — Portal Monitoring (after successful connect)
3101
+ KeyCode::Char('m') | KeyCode::Char('M')
3102
+ if self.state.connection_portal.connection_success
3103
+ || (matches!(self.state.operation_mode, OperationMode::Connected)
3104
+ && self.state.gateway_url.is_some()) =>
3105
+ {
3106
+ self.state
3107
+ .navigation
3108
+ .navigate_to_base(Screen::PortalMonitoring);
3109
+ self.state.advanced_monitoring =
3110
+ AdvancedMonitoringState::new(self.state.gateway_url.clone());
3111
+ self.state.portal_monitoring.scroll_offset = 0;
3112
+ self.add_log("[NAV] Opening Portal Monitoring".to_string());
3113
+ if let Some(ws) = ws_client {
3114
+ self.begin_portal_observability_request(ws);
3115
+ }
3116
+ self.request_immediate_render("open_portal_monitoring");
3117
+ }
3118
+
3119
+ // D — Disconnect Gateway (drop CLI link; reconnect with Enter after)
3120
+ KeyCode::Char('d') | KeyCode::Char('D') if self.gateway_session_linked() => {
3121
+ self.request_gateway_disconnect(ws_client);
3122
+ self.request_immediate_render("portal_disconnect");
3123
+ }
3124
+
3125
+ // R — Re-verify /health + /ready (detect stale link when Docker/Gateway stopped)
3126
+ KeyCode::Char('r') | KeyCode::Char('R') if self.gateway_session_linked() => {
3127
+ self.state.gateway_healthy = false;
3128
+ self.request_gateway_health_check(ws_client);
3129
+ self.request_immediate_render("portal_reverify");
3130
+ }
3131
+
3132
+ // L - Continue in local-only mode (no OS Server connection)
3133
+ KeyCode::Char('l') | KeyCode::Char('L') => {
3134
+ if self.gateway_session_linked() {
3135
+ self.request_gateway_disconnect(ws_client);
3136
+ }
3137
+ use crate::app::OperationMode;
3138
+ self.state.operation_mode = OperationMode::Local;
3139
+ self.state.gateway_url = None;
3140
+ self.state.connected = false;
3141
+ self.state.gateway_healthy = false;
3142
+ self.state.gateway_db_summary = None;
3143
+ self.state.connection_portal.finish_connecting();
3144
+ self.state.connection_portal.connection_success = false;
3145
+ self.state.connection_portal.error = None;
3146
+ self.state.pending_gateway_connect_id = None;
3147
+ self.state.pending_gateway_health_id = None;
3148
+ self.state.pending_gateway_observability_id = None;
3149
+ self.state.pending_monitoring_refresh_id = None;
3150
+ self.state.pending_monitoring_logs_id = None;
3151
+ self.state.pending_monitoring_drill_id = None;
3152
+ self.state.portal_monitoring = PortalMonitoringState::default();
3153
+ self.state.advanced_monitoring = AdvancedMonitoringState::default();
3154
+ self.state.portal_observability_last_poll = None;
3155
+ self.state.navigation.navigate_to_base(Screen::Main);
3156
+ self.add_log("[MODE] Continuing in LOCAL mode (no OS Server connection)".to_string());
3157
+ self.request_immediate_render("portal_local_mode");
3158
+ }
3159
+
3160
+ // Tab - Switch between URL and Username fields
3161
+ KeyCode::Tab | KeyCode::BackTab => {
3162
+ if self.state.connection_portal.connection_success {
3163
+ return Ok(false);
3164
+ }
3165
+ self.state.connection_portal.toggle_field();
3166
+ self.request_immediate_render("portal_field_toggle");
3167
+ }
3168
+
3169
+ // Up/Down - Navigate between fields
3170
+ KeyCode::Up | KeyCode::Down => {
3171
+ if self.state.connection_portal.connection_success {
3172
+ return Ok(false);
3173
+ }
3174
+ self.state.connection_portal.toggle_field();
3175
+ self.request_immediate_render("portal_navigate");
3176
+ }
3177
+
3178
+ // Enter - Connect to OS Server
3179
+ KeyCode::Enter => {
3180
+ let raw = self.state.connection_portal.gateway_url.clone();
3181
+ let url = raw
3182
+ .lines()
3183
+ .map(str::trim)
3184
+ .find(|l| !l.is_empty())
3185
+ .unwrap_or("")
3186
+ .to_string();
3187
+ self.state.connection_portal.gateway_url = url.clone();
3188
+
3189
+ // Validate URL
3190
+ if url.is_empty() {
3191
+ self.state.connection_portal.error =
3192
+ Some("OS Server URL is required".to_string());
3193
+ self.request_immediate_render("portal_error");
3194
+ return Ok(false);
3195
+ }
3196
+
3197
+ if !url.starts_with("http://") && !url.starts_with("https://") {
3198
+ self.state.connection_portal.error =
3199
+ Some("URL must start with http:// or https://".to_string());
3200
+ self.request_immediate_render("portal_error");
3201
+ return Ok(false);
3202
+ }
3203
+
3204
+ // Connect to OS Server
3205
+ self.state.connection_portal.error = None;
3206
+ self.state.connection_portal.begin_connecting();
3207
+
3208
+ if let Some(ws) = ws_client {
3209
+ let connect_data = serde_json::json!({
3210
+ "url": url,
3211
+ "username": self.state.connection_portal.username
3212
+ });
3213
+
3214
+ match ws.send_command("gateway.connect", Some(connect_data)) {
3215
+ Ok(id) => {
3216
+ let short_id = &id[id.len().saturating_sub(8)..];
3217
+ self.add_log(format!(
3218
+ "[GATEWAY] Connecting to {} (id: {})",
3219
+ url, short_id
3220
+ ));
3221
+ self.state.pending_gateway_connect_id = Some(id);
3222
+ }
3223
+ Err(e) => {
3224
+ self.state.connection_portal.finish_connecting();
3225
+ self.state.connection_portal.error =
3226
+ Some(format!("Failed to connect: {}", e));
3227
+ self.add_log(format!("[ERROR] OS Server connection failed: {}", e));
3228
+ }
3229
+ }
3230
+ } else {
3231
+ self.state.connection_portal.finish_connecting();
3232
+ self.state.connection_portal.error =
3233
+ Some("WebSocket not connected".to_string());
3234
+ self.add_log("[ERROR] WebSocket not connected".to_string());
3235
+ }
3236
+
3237
+ // CRITICAL: Use immediate render for instant status update
3238
+ self.request_immediate_render("portal_connect");
3239
+ }
3240
+
3241
+ // Typing - edit focused field (immediate render for responsiveness)
3242
+ KeyCode::Char(c) => {
3243
+ if self.state.connection_portal.connection_success {
3244
+ // After connect, URL field is read-only; use M for monitoring, ESC to leave.
3245
+ return Ok(false);
3246
+ }
3247
+ match self.state.connection_portal.focused_field {
3248
+ PortalField::GatewayUrl => {
3249
+ self.state.connection_portal.gateway_url.push(c);
3250
+ self.state.connection_portal.error = None;
3251
+ // OPTIMIZATION: Immediate render for instant typing feedback
3252
+ self.request_immediate_render("portal_input");
3253
+ }
3254
+ PortalField::Username => {
3255
+ self.state.connection_portal.username.push(c);
3256
+ self.state.connection_portal.error = None;
3257
+ // OPTIMIZATION: Immediate render for instant typing feedback
3258
+ self.request_immediate_render("portal_input");
3259
+ }
3260
+ }
3261
+ }
3262
+
3263
+ // Backspace - delete character (immediate render for responsiveness)
3264
+ KeyCode::Backspace => {
3265
+ if self.state.connection_portal.connection_success {
3266
+ return Ok(false);
3267
+ }
3268
+ match self.state.connection_portal.focused_field {
3269
+ PortalField::GatewayUrl => {
3270
+ self.state.connection_portal.gateway_url.pop();
3271
+ }
3272
+ PortalField::Username => {
3273
+ self.state.connection_portal.username.pop();
3274
+ }
3275
+ }
3276
+ self.state.connection_portal.error = None;
3277
+ // OPTIMIZATION: Immediate render for instant feedback
3278
+ self.request_immediate_render("portal_delete");
3279
+ }
3280
+
3281
+ _ => {}
3282
+ }
3283
+
3284
+ Ok(false)
3285
+ }
3286
+
3287
+ fn handle_portal_monitoring_input(
3288
+ &mut self,
3289
+ key: KeyEvent,
3290
+ ws_client: Option<&WebSocketClient>,
3291
+ ) -> anyhow::Result<bool> {
3292
+ use crate::ui::portal_monitoring::{
3293
+ portal_monitoring_scroll_max, portal_monitoring_section_offset,
3294
+ };
3295
+ use crossterm::event::KeyModifiers;
3296
+ if key.modifiers.contains(KeyModifiers::CONTROL) {
3297
+ match key.code {
3298
+ KeyCode::Char('c') | KeyCode::Char('q') => return Ok(true),
3299
+ _ => return Ok(false),
3300
+ }
3301
+ }
3302
+ if key.code == KeyCode::F(10) {
3303
+ return Ok(true);
3304
+ }
3305
+ if key.kind != KeyEventKind::Press {
3306
+ return Ok(false);
3307
+ }
3308
+ if self.state.portal_monitoring.help_visible {
3309
+ if key.code == KeyCode::Esc || key.code == KeyCode::Char('?') {
3310
+ self.state.portal_monitoring.help_visible = false;
3311
+ self.request_immediate_render("portal_help_close");
3312
+ }
3313
+ return Ok(false);
3314
+ }
3315
+ if key.code == KeyCode::Esc {
3316
+ self.state.pending_gateway_observability_id = None;
3317
+ self.state.pending_monitoring_refresh_id = None;
3318
+ self.state.pending_monitoring_logs_id = None;
3319
+ self.state.pending_monitoring_drill_id = None;
3320
+ self.state.portal_monitoring.section_refresh_loading = None;
3321
+ self.state.portal_monitoring.logs_fetch_loading = false;
3322
+ self.state.portal_monitoring.metrics_drill = None;
3323
+ self.state.portal_monitoring.metrics_drill_lines.clear();
3324
+ self.state.portal_monitoring.metrics_drill_loading = false;
3325
+ self.state.navigation.navigate_to_base(Screen::Main);
3326
+ self.add_log("[NAV] Portal Monitoring closed".to_string());
3327
+ self.request_immediate_render("portal_monitoring_close");
3328
+ return Ok(false);
3329
+ }
3330
+ if key.code == KeyCode::Char('?') {
3331
+ self.state.portal_monitoring.help_visible = true;
3332
+ self.request_immediate_render("portal_help_open");
3333
+ return Ok(false);
3334
+ }
3335
+ // R is deliberately global: refresh the full snapshot without changing the current view.
3336
+ if key.code == KeyCode::Char('r') || key.code == KeyCode::Char('R') {
3337
+ if let Some(ws) = ws_client {
3338
+ self.state.portal_observability_last_poll = None;
3339
+ self.cancel_portal_monitoring_requests();
3340
+ self.state.portal_monitoring.last_refresh = Some(std::time::Instant::now());
3341
+ self.begin_portal_observability_request(ws);
3342
+ self.request_immediate_render("portal_obs_refresh");
3343
+ } else {
3344
+ self.state.portal_monitoring.error =
3345
+ Some("WebSocket to CLI is not connected.".to_string());
3346
+ self.request_immediate_render("portal_obs_no_ws");
3347
+ }
3348
+ return Ok(false);
3349
+ }
3350
+ if key.code == KeyCode::Char('l') || key.code == KeyCode::Char('L') {
3351
+ let logs_open = self
3352
+ .state
3353
+ .advanced_monitoring
3354
+ .monitoring_state
3355
+ .selected_section
3356
+ == Some(MonitoringSection::Logs)
3357
+ && self
3358
+ .state
3359
+ .advanced_monitoring
3360
+ .monitoring_state
3361
+ .sections
3362
+ .get(&MonitoringSection::Logs)
3363
+ .map(|section| section.expanded)
3364
+ .unwrap_or(false);
3365
+ if logs_open && !self.state.portal_monitoring.logs_fetch_loading {
3366
+ self.state
3367
+ .advanced_monitoring
3368
+ .monitoring_state
3369
+ .collapse_section(MonitoringSection::Logs);
3370
+ self.state.portal_monitoring.scroll_offset = 0;
3371
+ self.request_immediate_render("portal_monitoring_logs_close");
3372
+ return Ok(false);
3373
+ }
3374
+ self.state
3375
+ .advanced_monitoring
3376
+ .monitoring_state
3377
+ .select_section(MonitoringSection::Logs);
3378
+ self.state
3379
+ .advanced_monitoring
3380
+ .monitoring_state
3381
+ .expand_section(MonitoringSection::Logs);
3382
+ if let Some(ws) = ws_client {
3383
+ self.cancel_portal_monitoring_requests();
3384
+ self.begin_monitoring_logs_request(ws);
3385
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
3386
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
3387
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
3388
+ &self.state,
3389
+ sw,
3390
+ inner_h,
3391
+ MonitoringSection::Logs,
3392
+ );
3393
+ self.request_immediate_render("portal_monitoring_logs");
3394
+ } else {
3395
+ self.state.portal_monitoring.error =
3396
+ Some("WebSocket to CLI is not connected.".to_string());
3397
+ self.request_immediate_render("portal_obs_no_ws");
3398
+ }
3399
+ return Ok(false);
3400
+ }
3401
+
3402
+ if key.code == KeyCode::Char('h') || key.code == KeyCode::Char('H') {
3403
+ self.state
3404
+ .advanced_monitoring
3405
+ .monitoring_state
3406
+ .select_section(MonitoringSection::Metrics);
3407
+ self.state
3408
+ .advanced_monitoring
3409
+ .monitoring_state
3410
+ .expand_section(MonitoringSection::Metrics);
3411
+ self.state.portal_monitoring.trend_view = !self.state.portal_monitoring.trend_view;
3412
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
3413
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
3414
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
3415
+ &self.state,
3416
+ sw,
3417
+ inner_h,
3418
+ MonitoringSection::Metrics,
3419
+ );
3420
+ let status = if self.state.portal_monitoring.trend_view {
3421
+ "enabled"
3422
+ } else {
3423
+ "disabled"
3424
+ };
3425
+ self.add_log(format!("[PORTAL] Trend/history view {}", status));
3426
+ self.request_immediate_render("portal_trend_toggle");
3427
+ return Ok(false);
3428
+ }
3429
+
3430
+ if key.code == KeyCode::Char('e') || key.code == KeyCode::Char('E') {
3431
+ match self.export_portal_monitoring_snapshot() {
3432
+ Ok(path) => {
3433
+ self.add_log(format!("[PORTAL] Exported monitoring snapshot: {}", path));
3434
+ self.state.portal_monitoring.error = None;
3435
+ }
3436
+ Err(e) => {
3437
+ self.state.portal_monitoring.error = Some(format!("Export failed: {}", e));
3438
+ }
3439
+ }
3440
+ self.request_immediate_render("portal_export");
3441
+ return Ok(false);
3442
+ }
3443
+
3444
+ // Phase 3: extended dependency view (GET /api/monitoring/dependencies/detail)
3445
+ if key.code == KeyCode::Char('t') || key.code == KeyCode::Char('T') {
3446
+ self.state
3447
+ .advanced_monitoring
3448
+ .monitoring_state
3449
+ .select_section(MonitoringSection::Dependencies);
3450
+ self.state
3451
+ .advanced_monitoring
3452
+ .monitoring_state
3453
+ .expand_section(MonitoringSection::Dependencies);
3454
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
3455
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
3456
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
3457
+ &self.state,
3458
+ sw,
3459
+ inner_h,
3460
+ MonitoringSection::Dependencies,
3461
+ );
3462
+ if let Some(ws) = ws_client {
3463
+ self.cancel_portal_monitoring_requests();
3464
+ self.begin_dependencies_detail_drill(ws);
3465
+ self.request_immediate_render("portal_deps_drill");
3466
+ } else {
3467
+ self.state.portal_monitoring.error =
3468
+ Some("WebSocket to CLI is not connected.".to_string());
3469
+ self.request_immediate_render("portal_obs_no_ws");
3470
+ }
3471
+ return Ok(false);
3472
+ }
3473
+
3474
+ // Phase 4: diagnostics for the local CLI/TUI bridge + Gateway reachability.
3475
+ // S toggles: show diagnostics → hide (remove override). While loading, S aborts the
3476
+ // in-flight drill so a later S can fetch again (avoids orphaned request ids vs cancel+refetch).
3477
+ if key.code == KeyCode::Char('s') || key.code == KeyCode::Char('S') {
3478
+ self.state
3479
+ .advanced_monitoring
3480
+ .monitoring_state
3481
+ .select_section(MonitoringSection::System);
3482
+ self.state
3483
+ .advanced_monitoring
3484
+ .monitoring_state
3485
+ .expand_section(MonitoringSection::System);
3486
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
3487
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
3488
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
3489
+ &self.state,
3490
+ sw,
3491
+ inner_h,
3492
+ MonitoringSection::System,
3493
+ );
3494
+
3495
+ let pm = &mut self.state.portal_monitoring;
3496
+ let has_system_diag =
3497
+ pm.section_overrides.contains_key(&MonitoringSection::System);
3498
+ let system_loading = pm.section_refresh_loading == Some(MonitoringSection::System);
3499
+
3500
+ if has_system_diag && !system_loading {
3501
+ pm.section_overrides.remove(&MonitoringSection::System);
3502
+ self.request_immediate_render("portal_system_dismiss");
3503
+ return Ok(false);
3504
+ }
3505
+ if system_loading {
3506
+ self.state.pending_monitoring_drill_id = None;
3507
+ self.state.portal_monitoring.section_refresh_loading = None;
3508
+ self.request_immediate_render("portal_system_abort_fetch");
3509
+ return Ok(false);
3510
+ }
3511
+
3512
+ if let Some(ws) = ws_client {
3513
+ self.cancel_portal_monitoring_requests();
3514
+ self.begin_system_diagnostics_request(ws);
3515
+ self.request_immediate_render("portal_system_diagnostics");
3516
+ } else {
3517
+ self.state.portal_monitoring.error =
3518
+ Some("WebSocket to CLI is not connected.".to_string());
3519
+ self.request_immediate_render("portal_obs_no_ws");
3520
+ }
3521
+ return Ok(false);
3522
+ }
3523
+
3524
+ if key.code == KeyCode::Right {
3525
+ self.state
3526
+ .advanced_monitoring
3527
+ .monitoring_state
3528
+ .select_section(MonitoringSection::Metrics);
3529
+ self.state
3530
+ .advanced_monitoring
3531
+ .monitoring_state
3532
+ .expand_section(MonitoringSection::Metrics);
3533
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
3534
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
3535
+ self.state.portal_monitoring.scroll_offset = portal_monitoring_section_offset(
3536
+ &self.state,
3537
+ sw,
3538
+ inner_h,
3539
+ MonitoringSection::Metrics,
3540
+ );
3541
+ if let Some(ws) = ws_client {
3542
+ self.cancel_portal_monitoring_requests();
3543
+ let panel = self
3544
+ .state
3545
+ .portal_monitoring
3546
+ .metrics_drill
3547
+ .map(|p| p.next())
3548
+ .unwrap_or(MetricsDrillPanel::Http);
3549
+ self.begin_monitoring_drill_request(ws, panel);
3550
+ self.request_immediate_render("portal_metrics_drill");
3551
+ } else {
3552
+ self.state.portal_monitoring.error =
3553
+ Some("WebSocket to CLI is not connected.".to_string());
3554
+ self.request_immediate_render("portal_obs_no_ws");
3555
+ }
3556
+ return Ok(false);
3557
+ }
3558
+
3559
+ if key.code == KeyCode::Left {
3560
+ if self.state.portal_monitoring.metrics_drill.is_some() {
3561
+ self.state.portal_monitoring.metrics_drill = None;
3562
+ self.state.portal_monitoring.metrics_drill_lines.clear();
3563
+ self.state.portal_monitoring.metrics_drill_loading = false;
3564
+ self.request_immediate_render("portal_metrics_drill_exit");
3565
+ return Ok(false);
3566
+ }
3567
+ }
3568
+
3569
+ // Toggle auto-refresh with 'A' key
3570
+ if key.code == KeyCode::Char('a') || key.code == KeyCode::Char('A') {
3571
+ self.state.portal_monitoring.auto_refresh_enabled =
3572
+ !self.state.portal_monitoring.auto_refresh_enabled;
3573
+ self.state.advanced_monitoring.monitoring_state.live_mode =
3574
+ self.state.portal_monitoring.auto_refresh_enabled;
3575
+ let status = if self.state.portal_monitoring.auto_refresh_enabled {
3576
+ "enabled"
3577
+ } else {
3578
+ "disabled"
3579
+ };
3580
+ self.add_log(format!("[PORTAL] Auto-refresh {}", status));
3581
+ self.request_immediate_render("portal_obs_auto_toggle");
3582
+ return Ok(false);
3583
+ }
3584
+
3585
+ // Phase 1: Enter / Space — expand or collapse selected section
3586
+ if key.code == KeyCode::Enter || key.code == KeyCode::Char(' ') {
3587
+ let idx = self
3588
+ .state
3589
+ .advanced_monitoring
3590
+ .monitoring_state
3591
+ .selected_index;
3592
+ self.state
3593
+ .advanced_monitoring
3594
+ .monitoring_state
3595
+ .toggle_section_at_index(idx);
3596
+ self.request_immediate_render("portal_monitoring_toggle_section");
3597
+ return Ok(false);
3598
+ }
3599
+
3600
+ let inner_h = self.state.portal_monitoring.viewport_lines.max(5);
3601
+ let sw = self.state.portal_monitoring.summary_clip_width.max(24);
3602
+ let max_scroll = portal_monitoring_scroll_max(&self.state, sw, inner_h);
3603
+ let logs_selected_and_open = self
3604
+ .state
3605
+ .advanced_monitoring
3606
+ .monitoring_state
3607
+ .selected_section
3608
+ == Some(MonitoringSection::Logs)
3609
+ && self
3610
+ .state
3611
+ .advanced_monitoring
3612
+ .monitoring_state
3613
+ .sections
3614
+ .get(&MonitoringSection::Logs)
3615
+ .map(|section| section.expanded)
3616
+ .unwrap_or(false);
3617
+
3618
+ let system_selected_and_diagnostics = self
3619
+ .state
3620
+ .advanced_monitoring
3621
+ .monitoring_state
3622
+ .selected_section
3623
+ == Some(MonitoringSection::System)
3624
+ && self
3625
+ .state
3626
+ .advanced_monitoring
3627
+ .monitoring_state
3628
+ .sections
3629
+ .get(&MonitoringSection::System)
3630
+ .map(|section| section.expanded)
3631
+ .unwrap_or(false)
3632
+ && self
3633
+ .state
3634
+ .portal_monitoring
3635
+ .section_overrides
3636
+ .contains_key(&MonitoringSection::System);
3637
+
3638
+ match key.code {
3639
+ KeyCode::Up => {
3640
+ if logs_selected_and_open || system_selected_and_diagnostics {
3641
+ self.state.portal_monitoring.scroll_offset =
3642
+ self.state.portal_monitoring.scroll_offset.saturating_sub(1);
3643
+ self.request_immediate_render("portal_obs_scroll");
3644
+ } else {
3645
+ self.state
3646
+ .advanced_monitoring
3647
+ .monitoring_state
3648
+ .select_previous_wrapped();
3649
+ self.state.portal_monitoring.scroll_offset = 0;
3650
+ self.request_immediate_render("portal_monitoring_select");
3651
+ }
3652
+ }
3653
+ KeyCode::Down => {
3654
+ if logs_selected_and_open || system_selected_and_diagnostics {
3655
+ self.state.portal_monitoring.scroll_offset =
3656
+ (self.state.portal_monitoring.scroll_offset + 1).min(max_scroll);
3657
+ self.request_immediate_render("portal_obs_scroll");
3658
+ } else {
3659
+ self.state
3660
+ .advanced_monitoring
3661
+ .monitoring_state
3662
+ .select_next_wrapped();
3663
+ self.state.portal_monitoring.scroll_offset = 0;
3664
+ self.request_immediate_render("portal_monitoring_select");
3665
+ }
3666
+ }
3667
+ KeyCode::PageUp => {
3668
+ let step = inner_h.saturating_sub(3).max(1);
3669
+ self.state.portal_monitoring.scroll_offset = self
3670
+ .state
3671
+ .portal_monitoring
3672
+ .scroll_offset
3673
+ .saturating_sub(step);
3674
+ self.request_immediate_render("portal_obs_scroll");
3675
+ }
3676
+ KeyCode::PageDown => {
3677
+ let step = inner_h.saturating_sub(3).max(1);
3678
+ self.state.portal_monitoring.scroll_offset =
3679
+ (self.state.portal_monitoring.scroll_offset + step).min(max_scroll);
3680
+ self.request_immediate_render("portal_obs_scroll");
3681
+ }
3682
+ _ => {}
3683
+ }
3684
+ Ok(false)
3685
+ }
3686
+
3687
+ /// After Setup Portal: reset Connection Portal so stale success/logs/errors are gone, pre-fill
3688
+ /// URL from the selected row, and copy detection hints onto the Connection screen.
3689
+ fn open_connection_portal_from_setup(&mut self) {
3690
+ use GatewayOption::*;
3691
+ self.state.connection_portal.reset();
3692
+ let url = self.state.setup_portal.selected_option.default_url();
3693
+ self.state.connection_portal.gateway_url = url.to_string();
3694
+ self.state.connection_portal.username.clear();
3695
+ self.state.connection_portal.focused_field = PortalField::GatewayUrl;
3696
+ self.state.connection_portal.connection_success = false;
3697
+ self.state.connection_portal.finish_connecting();
3698
+
3699
+ if let Some(ref det) = self.state.setup_portal.detection_result {
3700
+ let now = wall_clock_hms();
3701
+ match self.state.setup_portal.selected_option {
3702
+ LocalBundle if !det.local_bundle.available => {
3703
+ self.state.connection_portal.add_log_entry(
3704
+ now,
3705
+ LogLevel::Warning,
3706
+ "Setup did not find a vendored local OS server. Install 4runr-os globally, run from the repo, or set your OS Server URL manually.".to_string()
3707
+ );
3708
+ }
3709
+ LocalBundle if det.local_bundle.available && !det.local_bundle.running => {
3710
+ self.state.connection_portal.add_log_entry(
3711
+ now,
3712
+ LogLevel::Info,
3713
+ "Local Gateway bundle detected. Start the OS Server (cd apps/os-server && npm start), then press Enter to connect.".to_string()
3714
+ );
3715
+ }
3716
+ CloudServer if !det.cloud_server.available => {
3717
+ self.state.connection_portal.add_log_entry(
3718
+ now,
3719
+ LogLevel::Warning,
3720
+ "4Runr Cloud is not available in this build (Coming soon). Use Local Bundle or enter a Custom / self-hosted OS Server URL.".to_string()
3721
+ );
3722
+ }
3723
+ CustomUrl if url.is_empty() => {
3724
+ self.state.connection_portal.add_log_entry(
3725
+ now,
3726
+ LogLevel::Info,
3727
+ "Type your OS Server URL (https://…), then press Enter to connect."
3728
+ .to_string(),
3729
+ );
3730
+ }
3731
+ _ => {}
3732
+ }
3733
+ } else if matches!(self.state.setup_portal.selected_option, CustomUrl) && url.is_empty() {
3734
+ let now = wall_clock_hms();
3735
+ self.state.connection_portal.add_log_entry(
3736
+ now,
3737
+ LogLevel::Info,
3738
+ "Type your OS Server URL, then press Enter to connect.".to_string(),
3739
+ );
3740
+ }
3741
+
3742
+ self.state
3743
+ .navigation
3744
+ .navigate_to_base(Screen::ConnectionPortal);
3745
+ }
3746
+
3747
+ // ============================================================
3748
+ // SETUP PORTAL INPUT HANDLING
3749
+ // ============================================================
3750
+
3751
+ fn handle_setup_portal_input(
3752
+ &mut self,
3753
+ key: KeyEvent,
3754
+ _ws_client: Option<&WebSocketClient>,
3755
+ ) -> anyhow::Result<bool> {
3756
+ // ESC always closes the portal (even when detecting), so user can always get out
3757
+ if key.code == KeyCode::Esc {
3758
+ self.state.setup_portal.detecting = false;
3759
+ self.state.setup_portal.detecting_since = None;
3760
+ self.state.navigation.navigate_to_base(Screen::Main);
3761
+ self.add_log("[NAV] Setup Portal closed".to_string());
3762
+ self.request_immediate_render("setup_portal_close");
3763
+ return Ok(false);
3764
+ }
3765
+
3766
+ // Belt-and-suspenders: only Press for navigation (ESC Release handled in main loop + above).
3767
+ if key.kind != KeyEventKind::Press {
3768
+ return Ok(false);
3769
+ }
3770
+
3771
+ // First scan (no cached result): wait before Enter; still allow ↑↓ so the UI matches behavior.
3772
+ if self.state.setup_portal.detecting && self.state.setup_portal.detection_result.is_none() {
3773
+ if matches!(key.code, KeyCode::Enter) {
3774
+ return Ok(false);
3775
+ }
3776
+ }
3777
+
3778
+ match key.code {
3779
+ // F2 - Go directly to Connection Portal (same pre-fill + reset as Enter)
3780
+ KeyCode::F(2) => {
3781
+ self.open_connection_portal_from_setup();
3782
+ self.add_log("[NAV] Opening Connection Portal (F2)...".to_string());
3783
+ self.request_immediate_render("open_connection_portal");
3784
+ }
3785
+
3786
+ // Up/Down - Navigate options (immediate render so windowed terminals always redraw)
3787
+ KeyCode::Up => {
3788
+ self.state.setup_portal.cycle_option(-1);
3789
+ self.request_immediate_render("setup_nav_up");
3790
+ }
3791
+
3792
+ KeyCode::Down => {
3793
+ self.state.setup_portal.cycle_option(1);
3794
+ self.request_immediate_render("setup_nav_down");
3795
+ }
3796
+
3797
+ // Enter - Connection Portal with pre-filled URL and fresh portal state
3798
+ KeyCode::Enter => {
3799
+ self.state.setup_portal.error = None;
3800
+ self.open_connection_portal_from_setup();
3801
+
3802
+ self.add_log(format!(
3803
+ "[SETUP] Selected {} - Opening Connection Portal",
3804
+ self.state.setup_portal.selected_option.as_str()
3805
+ ));
3806
+ self.request_immediate_render("setup_enter");
3807
+ }
3808
+
3809
+ _ => {}
3810
+ }
3811
+
3812
+ Ok(false)
3813
+ }
3814
+ }