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.
- package/README.md +2 -2
- package/SETUP.md +2 -2
- package/USER-PRIVACY.md +2 -2
- package/apps/os-server/DEPLOYMENT.md +426 -0
- package/apps/os-server/JWT-RATE-LIMIT-VALIDATION.md +462 -0
- package/apps/os-server/PERSISTENCE-AND-AUTH.md +227 -0
- package/apps/os-server/PHASE5-TESTING.md +222 -0
- package/apps/os-server/README.md +368 -0
- package/apps/os-server/TROUBLESHOOTING.md +541 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/gateway-cancel-adapter.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/gateway-cancel-adapter.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/gateway-cancel-adapter.js +28 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/gateway-cancel-adapter.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/redis-sentinel-publisher.d.ts +23 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/redis-sentinel-publisher.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/redis-sentinel-publisher.js +95 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/redis-sentinel-publisher.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/sentinel-event-stream.d.ts +14 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/sentinel-event-stream.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/sentinel-event-stream.js +69 -0
- package/apps/os-server/dist/apps/os-server/src/adapters/sentinel-event-stream.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/agents/definitions-simple.d.ts +10 -0
- package/apps/os-server/dist/apps/os-server/src/agents/definitions-simple.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/agents/definitions-simple.js +377 -0
- package/apps/os-server/dist/apps/os-server/src/agents/definitions-simple.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/agents/definitions.d.ts +14 -0
- package/apps/os-server/dist/apps/os-server/src/agents/definitions.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/agents/definitions.js +210 -0
- package/apps/os-server/dist/apps/os-server/src/agents/definitions.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/agents/local-model-provider.d.ts +25 -0
- package/apps/os-server/dist/apps/os-server/src/agents/local-model-provider.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/agents/local-model-provider.js +144 -0
- package/apps/os-server/dist/apps/os-server/src/agents/local-model-provider.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/agents/tools.d.ts +24 -0
- package/apps/os-server/dist/apps/os-server/src/agents/tools.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/agents/tools.js +110 -0
- package/apps/os-server/dist/apps/os-server/src/agents/tools.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/anthropic-provider.d.ts +14 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/anthropic-provider.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/anthropic-provider.js +139 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/anthropic-provider.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/index.d.ts +6 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/index.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/index.js +6 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/index.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/openai-provider.d.ts +14 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/openai-provider.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/openai-provider.js +136 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/openai-provider.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/provider-manager.d.ts +18 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/provider-manager.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/provider-manager.js +91 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/provider-manager.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/redis-credentials-store.d.ts +15 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/redis-credentials-store.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/redis-credentials-store.js +181 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/redis-credentials-store.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/types.d.ts +54 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/types.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/types.js +2 -0
- package/apps/os-server/dist/apps/os-server/src/ai-providers/types.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/config/persistence.d.ts +5 -0
- package/apps/os-server/dist/apps/os-server/src/config/persistence.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/config/persistence.js +14 -0
- package/apps/os-server/dist/apps/os-server/src/config/persistence.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/crypto/envelope.d.ts +28 -0
- package/apps/os-server/dist/apps/os-server/src/crypto/envelope.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/crypto/envelope.js +111 -0
- package/apps/os-server/dist/apps/os-server/src/crypto/envelope.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/crypto/mfa-storage.d.ts +6 -0
- package/apps/os-server/dist/apps/os-server/src/crypto/mfa-storage.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/crypto/mfa-storage.js +61 -0
- package/apps/os-server/dist/apps/os-server/src/crypto/mfa-storage.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/docker-manager.d.ts +21 -0
- package/apps/os-server/dist/apps/os-server/src/db/docker-manager.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/docker-manager.js +166 -0
- package/apps/os-server/dist/apps/os-server/src/db/docker-manager.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/init.d.ts +11 -0
- package/apps/os-server/dist/apps/os-server/src/db/init.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/init.js +202 -0
- package/apps/os-server/dist/apps/os-server/src/db/init.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/memory-db.d.ts +149 -0
- package/apps/os-server/dist/apps/os-server/src/db/memory-db.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/memory-db.js +295 -0
- package/apps/os-server/dist/apps/os-server/src/db/memory-db.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/prisma.d.ts +4 -0
- package/apps/os-server/dist/apps/os-server/src/db/prisma.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/prisma.js +31 -0
- package/apps/os-server/dist/apps/os-server/src/db/prisma.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/redis.d.ts +5 -0
- package/apps/os-server/dist/apps/os-server/src/db/redis.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/db/redis.js +69 -0
- package/apps/os-server/dist/apps/os-server/src/db/redis.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/agents-api.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/agents-api.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/agents-api.js +369 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/agents-api.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/metrics-parser.d.ts +46 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/metrics-parser.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/metrics-parser.js +114 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/metrics-parser.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/middleware.d.ts +9 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/middleware.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/middleware.js +23 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/middleware.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/routes.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/routes.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/routes.js +270 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/routes.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/tools-api.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/tools-api.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/tools-api.js +183 -0
- package/apps/os-server/dist/apps/os-server/src/devkit/tools-api.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/health/index.d.ts +36 -0
- package/apps/os-server/dist/apps/os-server/src/health/index.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/health/index.js +275 -0
- package/apps/os-server/dist/apps/os-server/src/health/index.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/index.d.ts +2 -0
- package/apps/os-server/dist/apps/os-server/src/index.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/index.js +1131 -0
- package/apps/os-server/dist/apps/os-server/src/index.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/metrics/index.d.ts +48 -0
- package/apps/os-server/dist/apps/os-server/src/metrics/index.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/metrics/index.js +296 -0
- package/apps/os-server/dist/apps/os-server/src/metrics/index.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/metrics/monitoring-detail.d.ts +18 -0
- package/apps/os-server/dist/apps/os-server/src/metrics/monitoring-detail.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/metrics/monitoring-detail.js +117 -0
- package/apps/os-server/dist/apps/os-server/src/metrics/monitoring-detail.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/auth.d.ts +5 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/auth.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/auth.js +64 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/auth.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/authApiKey.d.ts +4 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/authApiKey.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/authApiKey.js +105 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/authApiKey.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/authJwt.d.ts +12 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/authJwt.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/authJwt.js +75 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/authJwt.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/correlationId.d.ts +6 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/correlationId.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/correlationId.js +21 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/correlationId.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/ddos-protection.d.ts +13 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/ddos-protection.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/ddos-protection.js +202 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/ddos-protection.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/errorHandler.d.ts +14 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/errorHandler.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/errorHandler.js +98 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/errorHandler.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/log-capture.d.ts +2 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/log-capture.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/log-capture.js +63 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/log-capture.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/mfa.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/mfa.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/mfa.js +96 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/mfa.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/rateLimit.d.ts +6 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/rateLimit.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/rateLimit.js +54 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/rateLimit.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/rbac.d.ts +19 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/rbac.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/rbac.js +229 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/rbac.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/security.d.ts +10 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/security.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/security.js +146 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/security.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/validate.d.ts +7 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/validate.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/validate.js +156 -0
- package/apps/os-server/dist/apps/os-server/src/middleware/validate.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/queue/config.d.ts +26 -0
- package/apps/os-server/dist/apps/os-server/src/queue/config.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/queue/config.js +21 -0
- package/apps/os-server/dist/apps/os-server/src/queue/config.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/queue/index.d.ts +13 -0
- package/apps/os-server/dist/apps/os-server/src/queue/index.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/queue/index.js +152 -0
- package/apps/os-server/dist/apps/os-server/src/queue/index.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/queue/processor.d.ts +16 -0
- package/apps/os-server/dist/apps/os-server/src/queue/processor.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/queue/processor.js +378 -0
- package/apps/os-server/dist/apps/os-server/src/queue/processor.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/queue/sentinel-execute-watch.d.ts +7 -0
- package/apps/os-server/dist/apps/os-server/src/queue/sentinel-execute-watch.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/queue/sentinel-execute-watch.js +22 -0
- package/apps/os-server/dist/apps/os-server/src/queue/sentinel-execute-watch.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/ai-providers-simple.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/routes/ai-providers-simple.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/ai-providers-simple.js +175 -0
- package/apps/os-server/dist/apps/os-server/src/routes/ai-providers-simple.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/chats.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/routes/chats.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/chats.js +150 -0
- package/apps/os-server/dist/apps/os-server/src/routes/chats.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/gdpr.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/routes/gdpr.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/gdpr.js +252 -0
- package/apps/os-server/dist/apps/os-server/src/routes/gdpr.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/mfa.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/routes/mfa.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/mfa.js +240 -0
- package/apps/os-server/dist/apps/os-server/src/routes/mfa.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/monitoring.d.ts +16 -0
- package/apps/os-server/dist/apps/os-server/src/routes/monitoring.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/monitoring.js +168 -0
- package/apps/os-server/dist/apps/os-server/src/routes/monitoring.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/sentinel-policies.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/routes/sentinel-policies.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/sentinel-policies.js +209 -0
- package/apps/os-server/dist/apps/os-server/src/routes/sentinel-policies.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/sentinel-predictive.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/routes/sentinel-predictive.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/sentinel-predictive.js +88 -0
- package/apps/os-server/dist/apps/os-server/src/routes/sentinel-predictive.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/shield.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/routes/shield.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/shield.js +85 -0
- package/apps/os-server/dist/apps/os-server/src/routes/shield.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/tool-credentials.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/routes/tool-credentials.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/tool-credentials.js +209 -0
- package/apps/os-server/dist/apps/os-server/src/routes/tool-credentials.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/tool-proxy.d.ts +3 -0
- package/apps/os-server/dist/apps/os-server/src/routes/tool-proxy.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/routes/tool-proxy.js +206 -0
- package/apps/os-server/dist/apps/os-server/src/routes/tool-proxy.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/index.d.ts +5 -0
- package/apps/os-server/dist/apps/os-server/src/runs/index.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/index.js +19 -0
- package/apps/os-server/dist/apps/os-server/src/runs/index.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/memoryRunStore.d.ts +12 -0
- package/apps/os-server/dist/apps/os-server/src/runs/memoryRunStore.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/memoryRunStore.js +78 -0
- package/apps/os-server/dist/apps/os-server/src/runs/memoryRunStore.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/postgresRunStore.d.ts +15 -0
- package/apps/os-server/dist/apps/os-server/src/runs/postgresRunStore.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/postgresRunStore.js +148 -0
- package/apps/os-server/dist/apps/os-server/src/runs/postgresRunStore.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/run-kill.d.ts +20 -0
- package/apps/os-server/dist/apps/os-server/src/runs/run-kill.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/run-kill.js +69 -0
- package/apps/os-server/dist/apps/os-server/src/runs/run-kill.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/run-retention.d.ts +17 -0
- package/apps/os-server/dist/apps/os-server/src/runs/run-retention.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/run-retention.js +124 -0
- package/apps/os-server/dist/apps/os-server/src/runs/run-retention.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/runStore.d.ts +10 -0
- package/apps/os-server/dist/apps/os-server/src/runs/runStore.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/runStore.js +2 -0
- package/apps/os-server/dist/apps/os-server/src/runs/runStore.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/types.d.ts +44 -0
- package/apps/os-server/dist/apps/os-server/src/runs/types.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/runs/types.js +2 -0
- package/apps/os-server/dist/apps/os-server/src/runs/types.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/schemas/runs.d.ts +126 -0
- package/apps/os-server/dist/apps/os-server/src/schemas/runs.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/schemas/runs.js +63 -0
- package/apps/os-server/dist/apps/os-server/src/schemas/runs.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/agent-token.d.ts +51 -0
- package/apps/os-server/dist/apps/os-server/src/security/agent-token.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/agent-token.js +161 -0
- package/apps/os-server/dist/apps/os-server/src/security/agent-token.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/outbound-url.d.ts +2 -0
- package/apps/os-server/dist/apps/os-server/src/security/outbound-url.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/outbound-url.js +65 -0
- package/apps/os-server/dist/apps/os-server/src/security/outbound-url.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/sentinel-config-store.d.ts +14 -0
- package/apps/os-server/dist/apps/os-server/src/security/sentinel-config-store.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/sentinel-config-store.js +168 -0
- package/apps/os-server/dist/apps/os-server/src/security/sentinel-config-store.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/sentinel-run-failure.d.ts +25 -0
- package/apps/os-server/dist/apps/os-server/src/security/sentinel-run-failure.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/sentinel-run-failure.js +46 -0
- package/apps/os-server/dist/apps/os-server/src/security/sentinel-run-failure.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/shield-config.d.ts +14 -0
- package/apps/os-server/dist/apps/os-server/src/security/shield-config.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/security/shield-config.js +70 -0
- package/apps/os-server/dist/apps/os-server/src/security/shield-config.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/utils/circuit-breaker.d.ts +13 -0
- package/apps/os-server/dist/apps/os-server/src/utils/circuit-breaker.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/utils/circuit-breaker.js +72 -0
- package/apps/os-server/dist/apps/os-server/src/utils/circuit-breaker.js.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/utils/log-encryption.d.ts +10 -0
- package/apps/os-server/dist/apps/os-server/src/utils/log-encryption.d.ts.map +1 -0
- package/apps/os-server/dist/apps/os-server/src/utils/log-encryption.js +140 -0
- package/apps/os-server/dist/apps/os-server/src/utils/log-encryption.js.map +1 -0
- package/apps/os-server/docker-compose.local.yml +46 -0
- package/apps/os-server/find-gateway.sh +37 -0
- package/apps/os-server/jest.config.cjs +52 -0
- package/apps/os-server/package-lock.json +8809 -0
- package/apps/os-server/package.json +66 -0
- package/apps/os-server/scripts/setup-test-env.sh +67 -0
- package/apps/os-server/src/__tests__/integration/README.md +92 -0
- package/apps/os-server/src/__tests__/integration/helpers/test-server.ts +178 -0
- package/apps/os-server/src/__tests__/integration/sentinel.test.ts +364 -0
- package/apps/os-server/src/adapters/redis-sentinel-publisher.ts +176 -0
- package/apps/os-server/src/agents/definitions-simple.ts +536 -0
- package/apps/os-server/src/ai-providers/anthropic-provider.ts +194 -0
- package/apps/os-server/src/ai-providers/openai-provider.ts +193 -0
- package/apps/os-server/src/ai-providers/redis-credentials-store.ts +224 -0
- package/apps/os-server/src/db/docker-manager.ts +262 -0
- package/apps/os-server/src/db/init.ts +292 -0
- package/apps/os-server/src/devkit/routes.ts +349 -0
- package/apps/os-server/src/health/index.ts +375 -0
- package/apps/os-server/src/index.ts +1506 -0
- package/apps/os-server/src/middleware/correlationId.ts +56 -0
- package/apps/os-server/src/middleware/ddos-protection.ts +318 -0
- package/apps/os-server/src/middleware/log-capture.ts +92 -0
- package/apps/os-server/src/middleware/security.ts +222 -0
- package/apps/os-server/src/routes/ai-providers-simple.ts +220 -0
- package/apps/os-server/src/routes/gdpr.ts +337 -0
- package/apps/os-server/src/routes/monitoring.ts +315 -0
- package/apps/os-server/src/routes/sentinel-policies.ts +293 -0
- package/apps/os-server/src/routes/sentinel-predictive.ts +129 -0
- package/apps/os-server/src/routes/tool-proxy.ts +285 -0
- package/apps/os-server/src/security/outbound-url.ts +83 -0
- package/apps/os-server/update-and-test.sh +36 -0
- package/dist/boot-sequence.js +1 -1
- package/dist/boot-sequence.js.map +1 -1
- package/dist/gateway-api-key-bootstrap.js +1 -1
- package/dist/gateway-api-key-bootstrap.js.map +1 -1
- package/dist/gateway-prisma-bootstrap.d.ts +1 -1
- package/dist/gateway-prisma-bootstrap.js +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/local-gateway-pid.d.ts +1 -1
- package/dist/local-gateway-pid.js +1 -1
- package/dist/os-tui-binary-bootstrap.d.ts +18 -0
- package/dist/os-tui-binary-bootstrap.d.ts.map +1 -0
- package/dist/os-tui-binary-bootstrap.js +175 -0
- package/dist/os-tui-binary-bootstrap.js.map +1 -0
- package/dist/rust-check.js +2 -2
- package/dist/rust-check.js.map +1 -1
- package/dist/system-verify.js +8 -8
- package/dist/system-verify.js.map +1 -1
- package/dist/tui-handlers.js +12 -12
- package/dist/tui-handlers.js.map +1 -1
- package/dist/watchdog.js +73 -73
- package/os-tui/Cargo.lock +1118 -0
- package/os-tui/Cargo.toml +17 -0
- package/os-tui/bin/os-tui.js +63 -0
- package/os-tui/binaries/win32-x64/os-tui.exe +0 -0
- package/os-tui/src/app/render_scheduler.rs +111 -0
- package/os-tui/src/app.rs +3814 -0
- package/os-tui/src/debug_log.rs +131 -0
- package/os-tui/src/io/mod.rs +63 -0
- package/os-tui/src/io/protocol.rs +14 -0
- package/os-tui/src/io/stdio.rs +31 -0
- package/os-tui/src/io/ws.rs +25 -0
- package/os-tui/src/main.rs +2090 -0
- package/os-tui/src/monitoring/mod.rs +439 -0
- package/os-tui/src/screens/mod.rs +254 -0
- package/os-tui/src/storage/cache.rs +221 -0
- package/os-tui/src/storage/mod.rs +5 -0
- package/os-tui/src/ui/agent_builder.rs +1148 -0
- package/os-tui/src/ui/agent_list.rs +344 -0
- package/os-tui/src/ui/boot.rs +145 -0
- package/os-tui/src/ui/connection_portal.rs +839 -0
- package/os-tui/src/ui/help.rs +340 -0
- package/os-tui/src/ui/layout.rs +978 -0
- package/os-tui/src/ui/mod.rs +13 -0
- package/os-tui/src/ui/portal_monitoring.rs +1128 -0
- package/os-tui/src/ui/run_manager.rs +922 -0
- package/os-tui/src/ui/safe_viewport.rs +236 -0
- package/os-tui/src/ui/sentinel_config.rs +828 -0
- package/os-tui/src/ui/settings.rs +414 -0
- package/os-tui/src/ui/setup_portal.rs +547 -0
- package/os-tui/src/ui/shield_dashboard.rs +1081 -0
- package/os-tui/src/websocket.rs +315 -0
- package/package.json +121 -121
- package/prisma/schema.prisma +470 -470
- package/scripts/cleanup-build-artifacts.js +2 -2
- package/scripts/os-tools-smoke.cjs +549 -549
- package/scripts/postinstall-os-server.js +63 -0
- package/scripts/postinstall-os-tui.js +64 -0
- package/scripts/seed-os-server-api-key.cjs +77 -0
- package/scripts/setup.js +11 -11
- package/src/security/package-integrity.ts +188 -188
- package/src/security/ssrf-protection.ts +147 -147
- package/apps/gateway/DEPLOYMENT.md +0 -426
- package/apps/gateway/JWT-RATE-LIMIT-VALIDATION.md +0 -462
- package/apps/gateway/PERSISTENCE-AND-AUTH.md +0 -227
- package/apps/gateway/PHASE5-TESTING.md +0 -222
- package/apps/gateway/README.md +0 -368
- package/apps/gateway/TROUBLESHOOTING.md +0 -541
- package/apps/gateway/docker-compose.local.yml +0 -46
- package/apps/gateway/find-gateway.sh +0 -37
- package/apps/gateway/jest.config.cjs +0 -52
- package/apps/gateway/package-lock.json +0 -8810
- package/apps/gateway/package.json +0 -66
- package/apps/gateway/scripts/setup-test-env.sh +0 -67
- package/apps/gateway/src/__tests__/integration/README.md +0 -92
- package/apps/gateway/src/__tests__/integration/helpers/test-server.ts +0 -178
- package/apps/gateway/src/__tests__/integration/sentinel.test.ts +0 -364
- package/apps/gateway/src/adapters/redis-sentinel-publisher.ts +0 -176
- package/apps/gateway/src/agents/definitions-simple.ts +0 -536
- package/apps/gateway/src/ai-providers/anthropic-provider.ts +0 -194
- package/apps/gateway/src/ai-providers/openai-provider.ts +0 -193
- package/apps/gateway/src/ai-providers/redis-credentials-store.ts +0 -224
- package/apps/gateway/src/db/docker-manager.ts +0 -262
- package/apps/gateway/src/db/init.ts +0 -292
- package/apps/gateway/src/devkit/routes.ts +0 -349
- package/apps/gateway/src/health/index.ts +0 -375
- package/apps/gateway/src/index.ts +0 -1506
- package/apps/gateway/src/middleware/correlationId.ts +0 -56
- package/apps/gateway/src/middleware/ddos-protection.ts +0 -318
- package/apps/gateway/src/middleware/log-capture.ts +0 -92
- package/apps/gateway/src/middleware/security.ts +0 -222
- package/apps/gateway/src/routes/ai-providers-simple.ts +0 -220
- package/apps/gateway/src/routes/gdpr.ts +0 -337
- package/apps/gateway/src/routes/monitoring.ts +0 -315
- package/apps/gateway/src/routes/sentinel-policies.ts +0 -293
- package/apps/gateway/src/routes/sentinel-predictive.ts +0 -129
- package/apps/gateway/src/routes/tool-proxy.ts +0 -285
- package/apps/gateway/src/security/outbound-url.ts +0 -83
- package/apps/gateway/update-and-test.sh +0 -36
- package/mk3-tui/Cargo.lock +0 -1118
- package/mk3-tui/Cargo.toml +0 -17
- package/mk3-tui/bin/mk3-tui.js +0 -63
- package/mk3-tui/binaries/win32-x64/mk3-tui.exe +0 -0
- package/mk3-tui/src/app/render_scheduler.rs +0 -111
- package/mk3-tui/src/app.rs +0 -3813
- package/mk3-tui/src/debug_log.rs +0 -131
- package/mk3-tui/src/io/mod.rs +0 -63
- package/mk3-tui/src/io/protocol.rs +0 -14
- package/mk3-tui/src/io/stdio.rs +0 -31
- package/mk3-tui/src/io/ws.rs +0 -25
- package/mk3-tui/src/main.rs +0 -2090
- package/mk3-tui/src/monitoring/mod.rs +0 -439
- package/mk3-tui/src/screens/mod.rs +0 -254
- package/mk3-tui/src/storage/cache.rs +0 -221
- package/mk3-tui/src/storage/mod.rs +0 -5
- package/mk3-tui/src/ui/agent_builder.rs +0 -1148
- package/mk3-tui/src/ui/agent_list.rs +0 -344
- package/mk3-tui/src/ui/boot.rs +0 -145
- package/mk3-tui/src/ui/connection_portal.rs +0 -839
- package/mk3-tui/src/ui/help.rs +0 -340
- package/mk3-tui/src/ui/layout.rs +0 -978
- package/mk3-tui/src/ui/mod.rs +0 -13
- package/mk3-tui/src/ui/portal_monitoring.rs +0 -1128
- package/mk3-tui/src/ui/run_manager.rs +0 -913
- package/mk3-tui/src/ui/safe_viewport.rs +0 -236
- package/mk3-tui/src/ui/sentinel_config.rs +0 -828
- package/mk3-tui/src/ui/settings.rs +0 -414
- package/mk3-tui/src/ui/setup_portal.rs +0 -547
- package/mk3-tui/src/ui/shield_dashboard.rs +0 -1081
- package/mk3-tui/src/websocket.rs +0 -315
- package/scripts/postinstall-gateway.js +0 -63
- package/scripts/postinstall-mk3.js +0 -64
- package/scripts/seed-gateway-api-key.cjs +0 -77
- /package/apps/{gateway → os-server}/.dockerignore +0 -0
- /package/apps/{gateway → os-server}/.eslintrc.json +0 -0
- /package/apps/{gateway → os-server}/Dockerfile +0 -0
- /package/apps/{gateway → os-server}/Dockerfile.bak +0 -0
- /package/apps/{gateway → os-server}/ENV_VARIABLES.md +0 -0
- /package/apps/{gateway → os-server}/create-test-script.sh +0 -0
- /package/apps/{gateway → os-server}/debug-api-responses.sh +0 -0
- /package/apps/{gateway → os-server}/debug-failing-tests-v2.sh +0 -0
- /package/apps/{gateway → os-server}/debug-failing-tests.sh +0 -0
- /package/apps/{gateway → os-server}/debug-responses.sh +0 -0
- /package/apps/{gateway → os-server}/debug-test-responses.sh +0 -0
- /package/apps/{gateway → os-server}/debug-tests.sh +0 -0
- /package/apps/{gateway → os-server}/diagnose-test-failure.sh +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/gateway-cancel-adapter.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/gateway-cancel-adapter.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/gateway-cancel-adapter.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/gateway-cancel-adapter.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/redis-sentinel-publisher.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/redis-sentinel-publisher.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/redis-sentinel-publisher.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/redis-sentinel-publisher.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/sentinel-event-stream.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/sentinel-event-stream.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/sentinel-event-stream.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/adapters/sentinel-event-stream.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions-simple.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions-simple.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions-simple.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions-simple.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/definitions.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/local-model-provider.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/local-model-provider.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/local-model-provider.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/local-model-provider.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/tools.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/tools.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/tools.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/agents/tools.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/anthropic-provider.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/anthropic-provider.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/anthropic-provider.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/anthropic-provider.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/index.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/index.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/index.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/index.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/openai-provider.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/openai-provider.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/openai-provider.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/openai-provider.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/provider-manager.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/provider-manager.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/provider-manager.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/provider-manager.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/redis-credentials-store.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/redis-credentials-store.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/redis-credentials-store.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/redis-credentials-store.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/types.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/types.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/types.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/ai-providers/types.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/config/persistence.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/config/persistence.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/config/persistence.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/config/persistence.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/envelope.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/envelope.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/envelope.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/envelope.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/mfa-storage.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/mfa-storage.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/mfa-storage.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/crypto/mfa-storage.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/docker-manager.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/docker-manager.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/docker-manager.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/docker-manager.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/init.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/init.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/init.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/init.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/memory-db.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/memory-db.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/memory-db.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/memory-db.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/prisma.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/prisma.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/prisma.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/prisma.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/redis.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/redis.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/redis.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/db/redis.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/agents-api.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/agents-api.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/agents-api.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/agents-api.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/metrics-parser.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/metrics-parser.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/metrics-parser.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/metrics-parser.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/middleware.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/middleware.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/middleware.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/middleware.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/routes.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/routes.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/routes.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/routes.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/tools-api.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/tools-api.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/tools-api.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/devkit/tools-api.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/health/index.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/health/index.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/health/index.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/health/index.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/index.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/index.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/index.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/index.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/index.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/index.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/index.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/index.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/monitoring-detail.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/monitoring-detail.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/monitoring-detail.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/metrics/monitoring-detail.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/auth.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/auth.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/auth.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/auth.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authApiKey.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authApiKey.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authApiKey.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authApiKey.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authJwt.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authJwt.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authJwt.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/authJwt.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/correlationId.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/correlationId.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/correlationId.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/correlationId.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/ddos-protection.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/ddos-protection.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/ddos-protection.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/ddos-protection.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/errorHandler.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/errorHandler.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/errorHandler.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/errorHandler.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/log-capture.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/log-capture.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/log-capture.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/log-capture.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/mfa.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/mfa.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/mfa.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/mfa.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rateLimit.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rateLimit.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rateLimit.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rateLimit.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rbac.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rbac.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rbac.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/rbac.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/security.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/security.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/security.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/security.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/validate.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/validate.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/validate.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/middleware/validate.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/config.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/config.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/config.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/config.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/index.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/index.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/index.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/index.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/processor.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/processor.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/processor.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/processor.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/sentinel-execute-watch.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/sentinel-execute-watch.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/sentinel-execute-watch.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/queue/sentinel-execute-watch.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/ai-providers-simple.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/ai-providers-simple.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/ai-providers-simple.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/ai-providers-simple.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/chats.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/chats.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/chats.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/chats.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/gdpr.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/gdpr.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/gdpr.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/gdpr.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/mfa.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/mfa.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/mfa.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/mfa.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/monitoring.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/monitoring.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/monitoring.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/monitoring.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-policies.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-policies.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-policies.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-policies.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-predictive.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-predictive.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-predictive.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/sentinel-predictive.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/shield.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/shield.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/shield.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/shield.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-credentials.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-credentials.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-credentials.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-credentials.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-proxy.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-proxy.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-proxy.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/routes/tool-proxy.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/index.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/index.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/index.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/index.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/memoryRunStore.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/memoryRunStore.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/memoryRunStore.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/memoryRunStore.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/postgresRunStore.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/postgresRunStore.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/postgresRunStore.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/postgresRunStore.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-kill.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-kill.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-kill.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-kill.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-retention.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-retention.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-retention.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/run-retention.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/runStore.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/runStore.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/runStore.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/runStore.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/types.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/types.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/types.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/runs/types.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/schemas/runs.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/schemas/runs.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/schemas/runs.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/schemas/runs.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/agent-token.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/agent-token.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/agent-token.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/agent-token.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/outbound-url.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/outbound-url.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/outbound-url.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/outbound-url.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-config-store.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-config-store.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-config-store.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-config-store.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-run-failure.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-run-failure.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-run-failure.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/sentinel-run-failure.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/shield-config.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/shield-config.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/shield-config.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/security/shield-config.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/circuit-breaker.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/circuit-breaker.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/circuit-breaker.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/circuit-breaker.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/log-encryption.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/log-encryption.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/log-encryption.js +0 -0
- /package/apps/{gateway → os-server}/dist/apps/gateway/src/utils/log-encryption.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/index.js +0 -0
- /package/apps/{gateway → os-server}/dist/packages/adapters/redis/IdempotencyStore.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/packages/adapters/redis/IdempotencyStore.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/packages/adapters/redis/IdempotencyStore.js +0 -0
- /package/apps/{gateway → os-server}/dist/packages/adapters/redis/IdempotencyStore.js.map +0 -0
- /package/apps/{gateway → os-server}/dist/packages/middleware/idempotency/idempotency.d.ts +0 -0
- /package/apps/{gateway → os-server}/dist/packages/middleware/idempotency/idempotency.d.ts.map +0 -0
- /package/apps/{gateway → os-server}/dist/packages/middleware/idempotency/idempotency.js +0 -0
- /package/apps/{gateway → os-server}/dist/packages/middleware/idempotency/idempotency.js.map +0 -0
- /package/apps/{gateway → os-server}/load-tests/README.md +0 -0
- /package/apps/{gateway → os-server}/load-tests/k6-basic.js +0 -0
- /package/apps/{gateway → os-server}/load-tests/k6-rate-limit.js +0 -0
- /package/apps/{gateway → os-server}/minimal-test.sh +0 -0
- /package/apps/{gateway → os-server}/public/sentinel-dashboard.html +0 -0
- /package/apps/{gateway → os-server}/quick-debug.sh +0 -0
- /package/apps/{gateway → os-server}/scripts/seed-api-key.ts +0 -0
- /package/apps/{gateway → os-server}/scripts/verify-sentinel-persist.mjs +0 -0
- /package/apps/{gateway → os-server}/simple-test.sh +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/agent-token.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/auth.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/correlationId.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/devkit-api.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/integration/authentication.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/integration/e2e-comprehensive-fixed.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/integration/e2e-workflow.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/integration/idempotency.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/integration/postgres-persistence.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/integration/rate-limiting.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/integration/shield.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/log-capture.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/no-persistence-mode.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/outbound-url.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/rateLimit.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/run-kill.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/run-retention.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/sentinel-config-persist-http.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/sentinel-config-store.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/sentinel-events.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/sentinel-execute-watch.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/sentinel-policies.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/sentinel-publish-kill.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/sentinel-run-failure.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/shield-config.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/shield-health.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/shield-metrics.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/tool-proxy.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/validateSecrets.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/__tests__/validation.test.ts +0 -0
- /package/apps/{gateway → os-server}/src/adapters/gateway-cancel-adapter.ts +0 -0
- /package/apps/{gateway → os-server}/src/adapters/sentinel-event-stream.ts +0 -0
- /package/apps/{gateway → os-server}/src/agents/definitions.ts +0 -0
- /package/apps/{gateway → os-server}/src/agents/local-model-provider.ts +0 -0
- /package/apps/{gateway → os-server}/src/agents/tools.ts +0 -0
- /package/apps/{gateway → os-server}/src/ai-providers/index.ts +0 -0
- /package/apps/{gateway → os-server}/src/ai-providers/provider-manager.ts +0 -0
- /package/apps/{gateway → os-server}/src/ai-providers/types.ts +0 -0
- /package/apps/{gateway → os-server}/src/config/persistence.ts +0 -0
- /package/apps/{gateway → os-server}/src/crypto/envelope.ts +0 -0
- /package/apps/{gateway → os-server}/src/crypto/mfa-storage.ts +0 -0
- /package/apps/{gateway → os-server}/src/db/memory-db.ts +0 -0
- /package/apps/{gateway → os-server}/src/db/prisma.ts +0 -0
- /package/apps/{gateway → os-server}/src/db/redis.ts +0 -0
- /package/apps/{gateway → os-server}/src/devkit/agents-api.ts +0 -0
- /package/apps/{gateway → os-server}/src/devkit/metrics-parser.ts +0 -0
- /package/apps/{gateway → os-server}/src/devkit/middleware.ts +0 -0
- /package/apps/{gateway → os-server}/src/devkit/tools-api.ts +0 -0
- /package/apps/{gateway → os-server}/src/metrics/index.ts +0 -0
- /package/apps/{gateway → os-server}/src/metrics/monitoring-detail.ts +0 -0
- /package/apps/{gateway → os-server}/src/middleware/auth.ts +0 -0
- /package/apps/{gateway → os-server}/src/middleware/authApiKey.ts +0 -0
- /package/apps/{gateway → os-server}/src/middleware/authJwt.ts +0 -0
- /package/apps/{gateway → os-server}/src/middleware/errorHandler.ts +0 -0
- /package/apps/{gateway → os-server}/src/middleware/mfa.ts +0 -0
- /package/apps/{gateway → os-server}/src/middleware/rateLimit.ts +0 -0
- /package/apps/{gateway → os-server}/src/middleware/rbac.ts +0 -0
- /package/apps/{gateway → os-server}/src/middleware/validate.ts +0 -0
- /package/apps/{gateway → os-server}/src/middleware/validate.ts.backup +0 -0
- /package/apps/{gateway → os-server}/src/queue/config.ts +0 -0
- /package/apps/{gateway → os-server}/src/queue/index.ts +0 -0
- /package/apps/{gateway → os-server}/src/queue/processor.ts +0 -0
- /package/apps/{gateway → os-server}/src/queue/sentinel-execute-watch.ts +0 -0
- /package/apps/{gateway → os-server}/src/routes/chats.ts +0 -0
- /package/apps/{gateway → os-server}/src/routes/mfa.ts +0 -0
- /package/apps/{gateway → os-server}/src/routes/shield.ts +0 -0
- /package/apps/{gateway → os-server}/src/routes/tool-credentials.ts +0 -0
- /package/apps/{gateway → os-server}/src/runs/index.ts +0 -0
- /package/apps/{gateway → os-server}/src/runs/memoryRunStore.ts +0 -0
- /package/apps/{gateway → os-server}/src/runs/postgresRunStore.ts +0 -0
- /package/apps/{gateway → os-server}/src/runs/run-kill.ts +0 -0
- /package/apps/{gateway → os-server}/src/runs/run-retention.ts +0 -0
- /package/apps/{gateway → os-server}/src/runs/runStore.ts +0 -0
- /package/apps/{gateway → os-server}/src/runs/types.ts +0 -0
- /package/apps/{gateway → os-server}/src/schemas/runs.ts +0 -0
- /package/apps/{gateway → os-server}/src/security/agent-token.ts +0 -0
- /package/apps/{gateway → os-server}/src/security/sentinel-config-store.ts +0 -0
- /package/apps/{gateway → os-server}/src/security/sentinel-run-failure.ts +0 -0
- /package/apps/{gateway → os-server}/src/security/shield-config.ts +0 -0
- /package/apps/{gateway → os-server}/src/types/fastify-rate-limit.d.ts +0 -0
- /package/apps/{gateway → os-server}/src/utils/circuit-breaker.ts +0 -0
- /package/apps/{gateway → os-server}/src/utils/log-encryption.ts +0 -0
- /package/apps/{gateway → os-server}/test-all-individual.sh +0 -0
- /package/apps/{gateway → os-server}/test-all-phases-final.sh +0 -0
- /package/apps/{gateway → os-server}/test-all-phases-fixed-v2.sh +0 -0
- /package/apps/{gateway → os-server}/test-all-phases-fixed.sh +0 -0
- /package/apps/{gateway → os-server}/test-all-phases.sh +0 -0
- /package/apps/{gateway → os-server}/test-concurrency.sh +0 -0
- /package/apps/{gateway → os-server}/test-debug.sh +0 -0
- /package/apps/{gateway → os-server}/test-e2e.sh +0 -0
- /package/apps/{gateway → os-server}/test-extraction.sh +0 -0
- /package/apps/{gateway → os-server}/test-full-operations-final.sh +0 -0
- /package/apps/{gateway → os-server}/test-full-operations.sh +0 -0
- /package/apps/{gateway → os-server}/test-idempotency-only.sh +0 -0
- /package/apps/{gateway → os-server}/test-idempotency.sh +0 -0
- /package/apps/{gateway → os-server}/test-individual.sh +0 -0
- /package/apps/{gateway → os-server}/test-queue.sh +0 -0
- /package/apps/{gateway → os-server}/tsconfig.json +0 -0
- /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
|
+
}
|