alysis-code 0.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,1622 @@
|
|
|
1
|
+
"""Presentation-agnostic state machine for the full-screen TUI setup wizard.
|
|
2
|
+
|
|
3
|
+
The classic first-run wizard (:mod:`cli_impl.setup_wizard`) is a Rich-print +
|
|
4
|
+
prompt_toolkit-prompt flow. When ``ALYSIS_TUI`` is on we want the *onboarding*
|
|
5
|
+
to match the alt-screen launch experience too, so this module expresses the flow
|
|
6
|
+
as a step machine that renders a :class:`Screen` description. Unlike the classic
|
|
7
|
+
wizard's method-first branching, the TUI opens with a single merged "Connect a
|
|
8
|
+
Provider" list (subscription sign-ins and API-key providers side by side); the
|
|
9
|
+
selected row dispatches to the right branch — API-key/model setup or subscription OAuth — followed by workspace and
|
|
10
|
+
commit. The prompt_toolkit application in
|
|
11
|
+
:mod:`cli_impl.tui.setup_app` reads that description and routes key events back
|
|
12
|
+
into the flow; nothing here imports prompt_toolkit, so the whole thing is unit
|
|
13
|
+
testable by driving the public methods synchronously.
|
|
14
|
+
|
|
15
|
+
The wizard *logic* is reused wholesale from :mod:`cli_impl.setup_wizard` (preset
|
|
16
|
+
list, key/model validation, persistence) and the low-level sandbox/login modules,
|
|
17
|
+
so there is a single source of truth for "what setup does"; this module only owns
|
|
18
|
+
"how the TUI walks through it".
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import io
|
|
24
|
+
import os
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from dataclasses import dataclass, field, replace
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Literal
|
|
29
|
+
|
|
30
|
+
from rich.console import Console as _RichConsole
|
|
31
|
+
|
|
32
|
+
from ...config import AppConfig, ConfigError
|
|
33
|
+
from ...profile_presets import (
|
|
34
|
+
canonical_model_alias_for_preset,
|
|
35
|
+
make_profile_from_preset,
|
|
36
|
+
)
|
|
37
|
+
from ...profiles import ProfileSpec
|
|
38
|
+
from ...workspace_binding import WorkspaceBindingError, resolve_workspace_binding
|
|
39
|
+
from .. import setup_wizard as _wiz
|
|
40
|
+
|
|
41
|
+
# Reuse the wizard's result dataclasses so the persisted shapes stay identical.
|
|
42
|
+
_ExecutionStepResult = _wiz._ExecutionStepResult
|
|
43
|
+
_DelegatedAuthResult = _wiz._DelegatedAuthResult
|
|
44
|
+
_ProfileStepResult = _wiz._ProfileStepResult
|
|
45
|
+
_ApiKeyStepResult = _wiz._ApiKeyStepResult
|
|
46
|
+
_ModelStepResult = _wiz._ModelStepResult
|
|
47
|
+
_WorkspaceStepResult = _wiz._WorkspaceStepResult
|
|
48
|
+
_SandboxStepResult = _wiz._SandboxStepResult
|
|
49
|
+
|
|
50
|
+
Mode = Literal["list", "input", "busy", "confirm", "message", "done"]
|
|
51
|
+
Tone = Literal["ok", "warn", "err", "dim", "plain"]
|
|
52
|
+
|
|
53
|
+
# Stages that count toward the "Step N of M" progress label (the visible
|
|
54
|
+
# decisions the user makes).
|
|
55
|
+
_PROGRESS_STAGES = (
|
|
56
|
+
"welcome",
|
|
57
|
+
"connect_provider",
|
|
58
|
+
"api_key",
|
|
59
|
+
"model",
|
|
60
|
+
"workspace",
|
|
61
|
+
"sandbox_choice",
|
|
62
|
+
)
|
|
63
|
+
_SUBSCRIPTION_PROGRESS_STAGES = ("welcome", "connect_provider", "workspace")
|
|
64
|
+
_PROGRESS_LABELS = {
|
|
65
|
+
"welcome": "Welcome",
|
|
66
|
+
"connect_provider": "Provider",
|
|
67
|
+
"api_key": "API key",
|
|
68
|
+
"model": "Model",
|
|
69
|
+
"workspace": "Workspace",
|
|
70
|
+
"sandbox_choice": "Sandbox",
|
|
71
|
+
}
|
|
72
|
+
_PROGRESS_ALIASES = {
|
|
73
|
+
"provider_advanced": ("connect_provider", "Provider (advanced)"),
|
|
74
|
+
"custom_name": ("connect_provider", "Provider (custom)"),
|
|
75
|
+
"custom_url": ("connect_provider", "Provider (custom)"),
|
|
76
|
+
"custom_headers": ("connect_provider", "Provider (custom)"),
|
|
77
|
+
"validating_key": ("api_key", "API key"),
|
|
78
|
+
"custom_model": ("model", "Model"),
|
|
79
|
+
"validating_model": ("model", "Model"),
|
|
80
|
+
"model_thinking": ("model", "Reasoning"),
|
|
81
|
+
"model_not_found_confirm": ("model", "Model"),
|
|
82
|
+
"workspace_create_confirm": ("workspace", "Workspace"),
|
|
83
|
+
"checking_runtime_auth": ("workspace", "Subscription"),
|
|
84
|
+
"runtime_login_confirm": ("workspace", "Subscription"),
|
|
85
|
+
"runtime_logging_in": ("workspace", "Subscription"),
|
|
86
|
+
"diagnosing_sandbox": ("sandbox_choice", "Sandbox"),
|
|
87
|
+
"installing_sandbox": ("sandbox_choice", "Sandbox"),
|
|
88
|
+
"pulling_sandbox": ("sandbox_choice", "Sandbox"),
|
|
89
|
+
"disabling_sandbox": ("sandbox_choice", "Sandbox"),
|
|
90
|
+
"sandbox_pull_confirm": ("sandbox_choice", "Sandbox"),
|
|
91
|
+
}
|
|
92
|
+
_MAX_KEY_ATTEMPTS = _wiz._MAX_API_KEY_VALIDATION_ATTEMPTS
|
|
93
|
+
_POST_COMMIT_STAGES = frozenset(
|
|
94
|
+
{
|
|
95
|
+
"checking_runtime_auth",
|
|
96
|
+
"runtime_login_confirm",
|
|
97
|
+
"runtime_logging_in",
|
|
98
|
+
"diagnosing_sandbox",
|
|
99
|
+
"sandbox_choice",
|
|
100
|
+
"sandbox_pull_confirm",
|
|
101
|
+
"installing_sandbox",
|
|
102
|
+
"pulling_sandbox",
|
|
103
|
+
"disabling_sandbox",
|
|
104
|
+
"login_confirm",
|
|
105
|
+
"logging_in",
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Cheap stage → interaction-mode lookup so the application's key-binding filters
|
|
111
|
+
# don't have to build a full :class:`Screen` (and its row list) on every repaint.
|
|
112
|
+
_STAGE_MODE: dict[str, Mode] = {
|
|
113
|
+
"welcome": "message",
|
|
114
|
+
"complete": "message",
|
|
115
|
+
"fatal": "message",
|
|
116
|
+
"connect_provider": "list",
|
|
117
|
+
"provider_advanced": "list",
|
|
118
|
+
"model": "list",
|
|
119
|
+
"model_thinking": "list",
|
|
120
|
+
"sandbox_choice": "list",
|
|
121
|
+
"custom_name": "input",
|
|
122
|
+
"custom_url": "input",
|
|
123
|
+
"custom_headers": "input",
|
|
124
|
+
"api_key": "input",
|
|
125
|
+
"custom_model": "input",
|
|
126
|
+
"workspace": "input",
|
|
127
|
+
"workspace_create_confirm": "confirm",
|
|
128
|
+
"validating_key": "busy",
|
|
129
|
+
"validating_model": "busy",
|
|
130
|
+
"committing": "busy",
|
|
131
|
+
"checking_runtime_auth": "busy",
|
|
132
|
+
"runtime_logging_in": "busy",
|
|
133
|
+
"diagnosing_sandbox": "busy",
|
|
134
|
+
"installing_sandbox": "busy",
|
|
135
|
+
"pulling_sandbox": "busy",
|
|
136
|
+
"disabling_sandbox": "busy",
|
|
137
|
+
"logging_in": "busy",
|
|
138
|
+
"sandbox_pull_confirm": "confirm",
|
|
139
|
+
"model_not_found_confirm": "confirm",
|
|
140
|
+
"login_confirm": "confirm",
|
|
141
|
+
"runtime_login_confirm": "confirm",
|
|
142
|
+
"cancel_confirm": "confirm",
|
|
143
|
+
"done": "done",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class Row:
|
|
149
|
+
"""One selectable option in a ``list`` screen (mirrors the picker rows).
|
|
150
|
+
|
|
151
|
+
``kind`` distinguishes real options ("item") from visual-only rows: a
|
|
152
|
+
"header" renders as a dim section caption and a "spacer" as a blank line;
|
|
153
|
+
neither is selectable. ``tone`` optionally tints the description ("warn"
|
|
154
|
+
highlights a problem summary, e.g. a missing required setting).
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
label: str
|
|
158
|
+
description: str = ""
|
|
159
|
+
value: str = ""
|
|
160
|
+
current: bool = False
|
|
161
|
+
kind: str = "item"
|
|
162
|
+
tone: str = ""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class Screen:
|
|
167
|
+
"""A render description the application paints; pure data, no widgets."""
|
|
168
|
+
|
|
169
|
+
stage: str
|
|
170
|
+
mode: Mode
|
|
171
|
+
title: str = ""
|
|
172
|
+
subtitle: str = ""
|
|
173
|
+
# Free-form lines for ``message`` screens (welcome / complete / fatal).
|
|
174
|
+
lines: list[tuple[str, Tone]] = field(default_factory=list)
|
|
175
|
+
# ``list`` mode.
|
|
176
|
+
rows: list[Row] = field(default_factory=list)
|
|
177
|
+
index: int = 0
|
|
178
|
+
# Whether ``list`` rows carry "N." prefixes and answer 1-9 digit jumps.
|
|
179
|
+
numbered: bool = True
|
|
180
|
+
# ``input`` mode.
|
|
181
|
+
input_label: str = ""
|
|
182
|
+
input_default: str = ""
|
|
183
|
+
input_password: bool = False
|
|
184
|
+
# A status line shown under the body (validation result / busy detail).
|
|
185
|
+
status: str = ""
|
|
186
|
+
status_tone: Tone = "dim"
|
|
187
|
+
# ``busy`` mode: which executor the app should use to run :meth:`run_busy`.
|
|
188
|
+
busy_kind: Literal["thread", "terminal"] = "thread"
|
|
189
|
+
busy_label: str = ""
|
|
190
|
+
hint: str = ""
|
|
191
|
+
progress: str = ""
|
|
192
|
+
# ``confirm`` mode: which option Enter selects.
|
|
193
|
+
confirm_default: bool = False
|
|
194
|
+
# Set once the flow terminates so the app can exit with the right result.
|
|
195
|
+
success: bool | None = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class SetupFlow:
|
|
199
|
+
"""Drives the TUI setup wizard one screen at a time.
|
|
200
|
+
|
|
201
|
+
The application calls :meth:`screen` to render, the navigation/selection
|
|
202
|
+
methods on key events, and — whenever the current screen is ``busy`` —
|
|
203
|
+
:meth:`run_busy` on a worker (or in a restored terminal for ``terminal``
|
|
204
|
+
busy kinds). Every transition is synchronous and side-effect-explicit so
|
|
205
|
+
tests can walk the whole flow without a terminal.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
def __init__(self, *, report: Callable[[str], None] | None = None) -> None:
|
|
209
|
+
self._report = report or (lambda _msg: None)
|
|
210
|
+
self.stage = "welcome"
|
|
211
|
+
self.index = 0
|
|
212
|
+
self.status = ""
|
|
213
|
+
self.status_tone: Tone = "dim"
|
|
214
|
+
# Collected results.
|
|
215
|
+
self.execution_result: _ExecutionStepResult | None = None
|
|
216
|
+
self.profile_result: _ProfileStepResult | None = None
|
|
217
|
+
self.api_key_result: _ApiKeyStepResult | None = None
|
|
218
|
+
self.model_result: _ModelStepResult | None = None
|
|
219
|
+
self.workspace_result: _WorkspaceStepResult | None = None
|
|
220
|
+
self.sandbox_result: _SandboxStepResult | None = None
|
|
221
|
+
self.cfg: AppConfig | None = None
|
|
222
|
+
self.login_summary: str = ""
|
|
223
|
+
self.login_ok: bool = False
|
|
224
|
+
self.runtime_auth_summary: str = ""
|
|
225
|
+
self.runtime_auth_connected: bool | None = None
|
|
226
|
+
self.diagnostic_lines: list[str] = []
|
|
227
|
+
self.fatal_error: str = ""
|
|
228
|
+
self.success: bool | None = None
|
|
229
|
+
# Transient working state.
|
|
230
|
+
self._key_attempts = 0
|
|
231
|
+
self._custom_name = ""
|
|
232
|
+
self._custom_url = ""
|
|
233
|
+
self._custom_headers: dict[str, str] = {}
|
|
234
|
+
self._last_model_not_found = ""
|
|
235
|
+
self._sandbox_diag: Any = None
|
|
236
|
+
self._sandbox_plan: Any = None
|
|
237
|
+
self._resume_stage = "" # stage to return to after a cancel prompt
|
|
238
|
+
self._pending_profile: ProfileSpec | None = None
|
|
239
|
+
self._pending_key = ""
|
|
240
|
+
self._pending_workspace_path: Path | None = None
|
|
241
|
+
self._discovered_model_options: tuple[Any, ...] = ()
|
|
242
|
+
self._model_catalog_warning = ""
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------- helpers
|
|
245
|
+
|
|
246
|
+
def set_report(self, report: Callable[[str], None]) -> None:
|
|
247
|
+
self._report = report
|
|
248
|
+
|
|
249
|
+
def current_mode(self) -> Mode:
|
|
250
|
+
return _STAGE_MODE.get(self.stage, "message")
|
|
251
|
+
|
|
252
|
+
def busy_kind(self) -> str:
|
|
253
|
+
return (
|
|
254
|
+
"terminal"
|
|
255
|
+
if self.stage in {"installing_sandbox", "pulling_sandbox", "runtime_logging_in"}
|
|
256
|
+
else "thread"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _hosted_mimo(self) -> bool:
|
|
260
|
+
preset = self.profile_result.preset if self.profile_result else None
|
|
261
|
+
if preset is None:
|
|
262
|
+
return False
|
|
263
|
+
try:
|
|
264
|
+
from ...alysis_cloud import PROFILE_KEY
|
|
265
|
+
except Exception:
|
|
266
|
+
return False
|
|
267
|
+
return preset.key == PROFILE_KEY
|
|
268
|
+
|
|
269
|
+
def _progress(self, stage: str) -> str:
|
|
270
|
+
uses_subscription = (
|
|
271
|
+
self.execution_result is not None and self.execution_result.backend == "delegated"
|
|
272
|
+
)
|
|
273
|
+
progress_stages = _SUBSCRIPTION_PROGRESS_STAGES if uses_subscription else _PROGRESS_STAGES
|
|
274
|
+
if stage in _PROGRESS_ALIASES:
|
|
275
|
+
base_stage, label = _PROGRESS_ALIASES[stage]
|
|
276
|
+
if base_stage in progress_stages:
|
|
277
|
+
number = progress_stages.index(base_stage) + 1
|
|
278
|
+
return f"Step {number} of {len(progress_stages)} · {label}"
|
|
279
|
+
if stage in progress_stages:
|
|
280
|
+
number = progress_stages.index(stage) + 1
|
|
281
|
+
label = _PROGRESS_LABELS.get(stage, stage.title())
|
|
282
|
+
return f"Step {number} of {len(progress_stages)} · {label}"
|
|
283
|
+
return f"Finishing · {stage.replace('_', ' ')}"
|
|
284
|
+
|
|
285
|
+
def _set_status(self, text: str, tone: Tone = "dim") -> None:
|
|
286
|
+
self.status = text
|
|
287
|
+
self.status_tone = tone
|
|
288
|
+
|
|
289
|
+
# ------------------------------------------------------------- rendering
|
|
290
|
+
|
|
291
|
+
def screen(self) -> Screen:
|
|
292
|
+
builder = getattr(self, f"_screen_{self.stage}", None)
|
|
293
|
+
if builder is None:
|
|
294
|
+
return Screen(
|
|
295
|
+
stage=self.stage,
|
|
296
|
+
mode="message",
|
|
297
|
+
title="Setup",
|
|
298
|
+
lines=[(f"(unknown stage {self.stage})", "err")],
|
|
299
|
+
)
|
|
300
|
+
scr: Screen = builder()
|
|
301
|
+
scr.progress = self._progress(self.stage)
|
|
302
|
+
if not scr.status:
|
|
303
|
+
scr.status = self.status
|
|
304
|
+
scr.status_tone = self.status_tone
|
|
305
|
+
scr.index = self.index
|
|
306
|
+
scr.success = self.success
|
|
307
|
+
return scr
|
|
308
|
+
|
|
309
|
+
def _screen_welcome(self) -> Screen:
|
|
310
|
+
return Screen(
|
|
311
|
+
stage="welcome",
|
|
312
|
+
mode="message",
|
|
313
|
+
title="Welcome to Alysis Code",
|
|
314
|
+
subtitle="A coding agent that runs in your terminal.",
|
|
315
|
+
lines=[
|
|
316
|
+
("First pick the AI provider Alysis Code should connect to.", "plain"),
|
|
317
|
+
("", "plain"),
|
|
318
|
+
(
|
|
319
|
+
"One list covers everything: subscription sign-ins, API-key "
|
|
320
|
+
"providers, and local endpoints.",
|
|
321
|
+
"dim",
|
|
322
|
+
),
|
|
323
|
+
("Setup then asks for the workspace folder you want to work on.", "dim"),
|
|
324
|
+
],
|
|
325
|
+
hint="▶ Press Enter to begin",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def _screen_connect_provider(self) -> Screen:
|
|
329
|
+
# One merged list (Kilo-style): subscription sign-ins first, then
|
|
330
|
+
# API-key providers, with local/custom endpoints behind the advanced
|
|
331
|
+
# row. The description column carries the auth method so no upfront
|
|
332
|
+
# method choice is needed.
|
|
333
|
+
rows = [
|
|
334
|
+
Row(label=label, description=description, value=value)
|
|
335
|
+
for value, label, description in _wiz._connect_provider_picker_rows()
|
|
336
|
+
]
|
|
337
|
+
return Screen(
|
|
338
|
+
stage="connect_provider",
|
|
339
|
+
mode="list",
|
|
340
|
+
title="Connect a Provider",
|
|
341
|
+
subtitle="Pick the provider you want Alysis Code to use.",
|
|
342
|
+
rows=rows,
|
|
343
|
+
hint="↑↓ move · Enter select · Esc back",
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def _screen_provider_advanced(self) -> Screen:
|
|
347
|
+
rows = [
|
|
348
|
+
Row(label=_wiz.preset_selection_label(preset), value=preset.key)
|
|
349
|
+
for preset in _wiz._advanced_setup_presets()
|
|
350
|
+
]
|
|
351
|
+
return Screen(
|
|
352
|
+
stage="provider_advanced",
|
|
353
|
+
mode="list",
|
|
354
|
+
title="Advanced Provider Profile",
|
|
355
|
+
subtitle=(
|
|
356
|
+
"Local endpoints (Ollama, LM Studio, vLLM), custom URLs, legacy "
|
|
357
|
+
"OpenAI-compatible presets, and Alysis Code account sign-in."
|
|
358
|
+
),
|
|
359
|
+
rows=rows,
|
|
360
|
+
hint="↑↓ move · Enter select · Esc back",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def _screen_custom_name(self) -> Screen:
|
|
364
|
+
return Screen(
|
|
365
|
+
stage="custom_name",
|
|
366
|
+
mode="input",
|
|
367
|
+
title="Custom endpoint",
|
|
368
|
+
subtitle="Name this profile (lowercase, no spaces).",
|
|
369
|
+
input_label="Profile name",
|
|
370
|
+
input_default=self._custom_name or "custom",
|
|
371
|
+
hint="Enter next · Esc back",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def _screen_custom_url(self) -> Screen:
|
|
375
|
+
return Screen(
|
|
376
|
+
stage="custom_url",
|
|
377
|
+
mode="input",
|
|
378
|
+
title="Custom endpoint",
|
|
379
|
+
subtitle="The OpenAI-compatible base URL for this provider.",
|
|
380
|
+
input_label="Base URL",
|
|
381
|
+
input_default=self._custom_url,
|
|
382
|
+
hint="Enter next · Esc back",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
def _screen_custom_headers(self) -> Screen:
|
|
386
|
+
existing = ", ".join(f"{k}={v}" for k, v in (self._custom_headers or {}).items())
|
|
387
|
+
return Screen(
|
|
388
|
+
stage="custom_headers",
|
|
389
|
+
mode="input",
|
|
390
|
+
title="Custom endpoint",
|
|
391
|
+
subtitle="Extra request headers, if your endpoint needs them (org id, x-api-key…).",
|
|
392
|
+
input_label="Extra headers (k=v, comma-separated)",
|
|
393
|
+
input_default=existing or "(none — Enter to skip)",
|
|
394
|
+
hint="Enter next · Esc back",
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def _screen_api_key(self) -> Screen:
|
|
398
|
+
profile = self.profile_result.profile # type: ignore[union-attr]
|
|
399
|
+
required = bool(profile.api_key_env)
|
|
400
|
+
has_key = self.api_key_result is not None and bool(self.api_key_result.api_key)
|
|
401
|
+
if required:
|
|
402
|
+
label = "Paste your API key"
|
|
403
|
+
if has_key:
|
|
404
|
+
label += " (Enter to keep current)"
|
|
405
|
+
else:
|
|
406
|
+
label = "API key (optional — Enter to skip)"
|
|
407
|
+
sub = f"Alysis Code will use {self.profile_result.label}." # type: ignore[union-attr]
|
|
408
|
+
if profile.api_key_env:
|
|
409
|
+
sub += f" The key can also be set via {profile.api_key_env}."
|
|
410
|
+
return Screen(
|
|
411
|
+
stage="api_key",
|
|
412
|
+
mode="input",
|
|
413
|
+
title="API Key",
|
|
414
|
+
subtitle=sub,
|
|
415
|
+
input_label=label,
|
|
416
|
+
input_password=True,
|
|
417
|
+
hint="Enter validate · Esc back",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def _screen_model(self) -> Screen:
|
|
421
|
+
rows: list[Row] = []
|
|
422
|
+
for value, label, description in _wiz._model_picker_rows( # type: ignore[arg-type]
|
|
423
|
+
self.profile_result,
|
|
424
|
+
discovered_models=self._discovered_model_options,
|
|
425
|
+
):
|
|
426
|
+
rows.append(Row(label=label, description=description, value=value))
|
|
427
|
+
return Screen(
|
|
428
|
+
stage="model",
|
|
429
|
+
mode="list",
|
|
430
|
+
title="Default Model",
|
|
431
|
+
subtitle=f"Pick the model Alysis Code will use by default for {self.profile_result.label}.", # type: ignore[union-attr]
|
|
432
|
+
rows=rows,
|
|
433
|
+
status=self._model_catalog_warning,
|
|
434
|
+
status_tone="warn" if self._model_catalog_warning else "dim",
|
|
435
|
+
hint="↑↓ move · 1-9 pick · Enter select · Esc back",
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
def _screen_model_thinking(self) -> Screen:
|
|
439
|
+
model = self.model_result.model if self.model_result is not None else ""
|
|
440
|
+
current = self.model_result.reasoning_label if self.model_result is not None else "auto"
|
|
441
|
+
labels = _wiz._setup_reasoning_labels( # type: ignore[arg-type]
|
|
442
|
+
self.profile_result,
|
|
443
|
+
model=model,
|
|
444
|
+
current=current,
|
|
445
|
+
)
|
|
446
|
+
descriptions = {
|
|
447
|
+
"off": "disable reasoning with this model's documented NVIDIA control",
|
|
448
|
+
"low": "lower reasoning effort",
|
|
449
|
+
"medium": "balanced reasoning effort",
|
|
450
|
+
"high": "full reasoning effort",
|
|
451
|
+
"max": "maximum reasoning effort",
|
|
452
|
+
"auto": "use the NVIDIA-hosted model's default",
|
|
453
|
+
}
|
|
454
|
+
return Screen(
|
|
455
|
+
stage="model_thinking",
|
|
456
|
+
mode="list",
|
|
457
|
+
title="Reasoning Effort",
|
|
458
|
+
subtitle=f"Verified reasoning modes for {model} on NVIDIA.",
|
|
459
|
+
rows=[
|
|
460
|
+
Row(
|
|
461
|
+
label=label,
|
|
462
|
+
description=descriptions.get(label, "provider-documented reasoning effort"),
|
|
463
|
+
value=label,
|
|
464
|
+
current=label == current,
|
|
465
|
+
)
|
|
466
|
+
for label in labels
|
|
467
|
+
],
|
|
468
|
+
hint="↑↓ move · Enter select · Esc back",
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
def _screen_custom_model(self) -> Screen:
|
|
472
|
+
return Screen(
|
|
473
|
+
stage="custom_model",
|
|
474
|
+
mode="input",
|
|
475
|
+
title="Default Model",
|
|
476
|
+
subtitle="Type any model id supported by this provider.",
|
|
477
|
+
input_label="Model",
|
|
478
|
+
input_default=self._last_model_not_found,
|
|
479
|
+
hint="Enter next · Esc back",
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
def _screen_workspace(self) -> Screen:
|
|
483
|
+
default = _wiz._suggest_workspace_default()
|
|
484
|
+
prev = self.workspace_result.workspace if self.workspace_result else os.fspath(default)
|
|
485
|
+
return Screen(
|
|
486
|
+
stage="workspace",
|
|
487
|
+
mode="input",
|
|
488
|
+
title="Workspace",
|
|
489
|
+
subtitle="Alysis Code reads and edits files inside one folder.",
|
|
490
|
+
input_label="Workspace folder",
|
|
491
|
+
input_default=prev,
|
|
492
|
+
hint="Enter confirm · Esc back",
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
def _screen_workspace_create_confirm(self) -> Screen:
|
|
496
|
+
pending = os.fspath(self._pending_workspace_path or "")
|
|
497
|
+
return Screen(
|
|
498
|
+
stage="workspace_create_confirm",
|
|
499
|
+
mode="confirm",
|
|
500
|
+
title="Workspace",
|
|
501
|
+
subtitle="That folder does not exist yet.",
|
|
502
|
+
lines=[(f"Create it now? {pending}", "plain")],
|
|
503
|
+
hint="Y create · N choose another · Esc back",
|
|
504
|
+
confirm_default=True,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# ----- busy screens (the app runs run_busy(), they transition themselves)
|
|
508
|
+
|
|
509
|
+
def _screen_validating_key(self) -> Screen:
|
|
510
|
+
return Screen(
|
|
511
|
+
stage="validating_key", mode="busy", title="API Key", busy_label="Validating key…"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
def _screen_validating_model(self) -> Screen:
|
|
515
|
+
return Screen(
|
|
516
|
+
stage="validating_model",
|
|
517
|
+
mode="busy",
|
|
518
|
+
title="Default Model",
|
|
519
|
+
busy_label="Validating model…",
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
def _screen_committing(self) -> Screen:
|
|
523
|
+
return Screen(stage="committing", mode="busy", title="Saving", busy_label="Saving setup…")
|
|
524
|
+
|
|
525
|
+
def _screen_checking_runtime_auth(self) -> Screen:
|
|
526
|
+
return Screen(
|
|
527
|
+
stage="checking_runtime_auth",
|
|
528
|
+
mode="busy",
|
|
529
|
+
title="AI subscription",
|
|
530
|
+
busy_label="Checking subscription connection…",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
def _screen_runtime_logging_in(self) -> Screen:
|
|
534
|
+
return Screen(
|
|
535
|
+
stage="runtime_logging_in",
|
|
536
|
+
mode="busy",
|
|
537
|
+
busy_kind="terminal",
|
|
538
|
+
title="AI subscription",
|
|
539
|
+
busy_label="Opening provider sign-in…",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
def _screen_diagnosing_sandbox(self) -> Screen:
|
|
543
|
+
return Screen(
|
|
544
|
+
stage="diagnosing_sandbox",
|
|
545
|
+
mode="busy",
|
|
546
|
+
title="Sandbox",
|
|
547
|
+
busy_label="Checking sandbox readiness…",
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
def _screen_installing_sandbox(self) -> Screen:
|
|
551
|
+
return Screen(
|
|
552
|
+
stage="installing_sandbox",
|
|
553
|
+
mode="busy",
|
|
554
|
+
busy_kind="terminal",
|
|
555
|
+
title="Sandbox",
|
|
556
|
+
busy_label="Installing bubblewrap… (watch the terminal — you may be asked for sudo)",
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
def _screen_pulling_sandbox(self) -> Screen:
|
|
560
|
+
return Screen(
|
|
561
|
+
stage="pulling_sandbox",
|
|
562
|
+
mode="busy",
|
|
563
|
+
busy_kind="terminal",
|
|
564
|
+
title="Sandbox",
|
|
565
|
+
busy_label="Pulling sandbox image… (this can take a minute)",
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
def _screen_disabling_sandbox(self) -> Screen:
|
|
569
|
+
return Screen(
|
|
570
|
+
stage="disabling_sandbox", mode="busy", title="Sandbox", busy_label="Disabling sandbox…"
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
def _screen_logging_in(self) -> Screen:
|
|
574
|
+
return Screen(
|
|
575
|
+
stage="logging_in", mode="busy", title="Alysis Code account", busy_label="Connecting…"
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# ----- confirm screens
|
|
579
|
+
|
|
580
|
+
def _screen_sandbox_choice(self) -> Screen:
|
|
581
|
+
rows: list[Row] = []
|
|
582
|
+
if self._sandbox_plan is not None:
|
|
583
|
+
rows.append(
|
|
584
|
+
Row(
|
|
585
|
+
label="Install bubblewrap now (keeps sandbox on)",
|
|
586
|
+
description=f"Runs: {self._sandbox_plan.display}",
|
|
587
|
+
value="install_bwrap",
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
rows.append(
|
|
591
|
+
Row(
|
|
592
|
+
label="Run without sandbox for now (less safe)",
|
|
593
|
+
description="Commands run on the host shell. Re-enable anytime in /config.",
|
|
594
|
+
value="disable",
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
rows.append(
|
|
598
|
+
Row(
|
|
599
|
+
label="Decide later",
|
|
600
|
+
description="Finish with `alysis sandbox setup`.",
|
|
601
|
+
value="later",
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
return Screen(
|
|
605
|
+
stage="sandbox_choice",
|
|
606
|
+
mode="list",
|
|
607
|
+
title="Sandbox",
|
|
608
|
+
subtitle="No sandbox backend (bubblewrap or Docker) was found. How do you want to proceed?",
|
|
609
|
+
rows=rows,
|
|
610
|
+
hint="↑↓ move · Enter select · Esc skip",
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
def _screen_sandbox_pull_confirm(self) -> Screen:
|
|
614
|
+
return Screen(
|
|
615
|
+
stage="sandbox_pull_confirm",
|
|
616
|
+
mode="confirm",
|
|
617
|
+
title="Sandbox",
|
|
618
|
+
subtitle="The sandbox image was not found locally.",
|
|
619
|
+
lines=[("Pull the Alysis Code sandbox image now? (~50MB)", "plain")],
|
|
620
|
+
hint="Y pull · N skip · Esc skip",
|
|
621
|
+
confirm_default=True,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
def _screen_model_not_found_confirm(self) -> Screen:
|
|
625
|
+
return Screen(
|
|
626
|
+
stage="model_not_found_confirm",
|
|
627
|
+
mode="confirm",
|
|
628
|
+
title="Default Model",
|
|
629
|
+
subtitle="The provider reported this model is missing.",
|
|
630
|
+
lines=[(f"Use custom model '{self._last_model_not_found}' anyway?", "warn")],
|
|
631
|
+
hint="Y use it · N pick again · Esc back",
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
def _screen_login_confirm(self) -> Screen:
|
|
635
|
+
return Screen(
|
|
636
|
+
stage="login_confirm",
|
|
637
|
+
mode="confirm",
|
|
638
|
+
title="Alysis Code account",
|
|
639
|
+
subtitle="Your provider is the Alysis Code-hosted MiMo endpoint.",
|
|
640
|
+
lines=[("Connect your Alysis Code account now?", "plain")],
|
|
641
|
+
hint="Y connect · N later · Esc skip",
|
|
642
|
+
confirm_default=True,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
def _screen_runtime_login_confirm(self) -> Screen:
|
|
646
|
+
runtime_label = (
|
|
647
|
+
self.execution_result.label if self.execution_result is not None else "AI subscription"
|
|
648
|
+
)
|
|
649
|
+
return Screen(
|
|
650
|
+
stage="runtime_login_confirm",
|
|
651
|
+
mode="confirm",
|
|
652
|
+
title="Connect subscription",
|
|
653
|
+
subtitle=f"{runtime_label} is not connected.",
|
|
654
|
+
lines=[
|
|
655
|
+
("Connect now using the provider's browser login?", "plain"),
|
|
656
|
+
(
|
|
657
|
+
"Provider login is an immediate external side effect. Cancelling or "
|
|
658
|
+
"leaving setup later does not undo it.",
|
|
659
|
+
"warn",
|
|
660
|
+
),
|
|
661
|
+
],
|
|
662
|
+
hint="Y connect · N later · Esc skip",
|
|
663
|
+
confirm_default=True,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
def _screen_cancel_confirm(self) -> Screen:
|
|
667
|
+
settings_saved = self._resume_stage in _POST_COMMIT_STAGES
|
|
668
|
+
return Screen(
|
|
669
|
+
stage="cancel_confirm",
|
|
670
|
+
mode="confirm",
|
|
671
|
+
title="Cancel setup",
|
|
672
|
+
lines=(
|
|
673
|
+
[
|
|
674
|
+
("Leave setup? Your model access settings are already saved.", "warn"),
|
|
675
|
+
("Leaving does not undo provider sign-in or sign-out actions.", "dim"),
|
|
676
|
+
]
|
|
677
|
+
if settings_saved
|
|
678
|
+
else [("Cancel setup? No changes will be saved.", "warn")]
|
|
679
|
+
),
|
|
680
|
+
hint="Y cancel · N keep going",
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
def _screen_complete(self) -> Screen:
|
|
684
|
+
lines = self._summary_lines()
|
|
685
|
+
delegated = self.cfg is not None and self.cfg.execution.backend == "delegated"
|
|
686
|
+
subscription_selection_required = False
|
|
687
|
+
if self.cfg is not None and not delegated:
|
|
688
|
+
try:
|
|
689
|
+
from ...profiles import active_subscription_selection_ready, get_active_profile
|
|
690
|
+
|
|
691
|
+
subscription_selection_required = bool(
|
|
692
|
+
get_active_profile(self.cfg).auth_provider
|
|
693
|
+
and not active_subscription_selection_ready(self.cfg)
|
|
694
|
+
)
|
|
695
|
+
except Exception:
|
|
696
|
+
subscription_selection_required = False
|
|
697
|
+
return Screen(
|
|
698
|
+
stage="complete",
|
|
699
|
+
mode="message",
|
|
700
|
+
title="Setup complete",
|
|
701
|
+
lines=lines,
|
|
702
|
+
hint=(
|
|
703
|
+
"▶ Press Enter to finish setup"
|
|
704
|
+
if delegated
|
|
705
|
+
else (
|
|
706
|
+
"▶ Press Enter to choose model and reasoning in /config"
|
|
707
|
+
if subscription_selection_required
|
|
708
|
+
else "▶ Press Enter to start chatting"
|
|
709
|
+
)
|
|
710
|
+
),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
def _screen_fatal(self) -> Screen:
|
|
714
|
+
return Screen(
|
|
715
|
+
stage="fatal",
|
|
716
|
+
mode="message",
|
|
717
|
+
title="Setup failed",
|
|
718
|
+
lines=[(self.fatal_error or "Setup could not be completed.", "err")],
|
|
719
|
+
hint="▶ Press Enter to exit",
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
# ------------------------------------------------------------- summaries
|
|
723
|
+
|
|
724
|
+
def _summary_lines(self) -> list[tuple[str, Tone]]:
|
|
725
|
+
out: list[tuple[str, Tone]] = []
|
|
726
|
+
delegated = (
|
|
727
|
+
self.execution_result is not None and self.execution_result.backend == "delegated"
|
|
728
|
+
)
|
|
729
|
+
if delegated and self.execution_result is not None:
|
|
730
|
+
out.append(("Connection AI subscription", "ok"))
|
|
731
|
+
out.append((f"Provider {self.execution_result.label}", "plain"))
|
|
732
|
+
auth_summary = self.runtime_auth_summary or "not connected"
|
|
733
|
+
out.append(
|
|
734
|
+
(
|
|
735
|
+
f"Sign-in {auth_summary}",
|
|
736
|
+
"ok" if self.runtime_auth_connected else "warn",
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
if self.profile_result is not None:
|
|
740
|
+
out.append((f"Profile {self.profile_result.label} (active)", "ok"))
|
|
741
|
+
if self.api_key_result is not None:
|
|
742
|
+
out.append((f"API key {self._api_key_summary()}", self._api_key_tone()))
|
|
743
|
+
if self.model_result is not None:
|
|
744
|
+
out.append((f"Model {self.model_result.model}", "plain"))
|
|
745
|
+
if (
|
|
746
|
+
self.profile_result is not None
|
|
747
|
+
and self.profile_result.preset is not None
|
|
748
|
+
and self.profile_result.preset.key in _wiz._MODEL_REASONING_SETUP_PRESET_KEYS
|
|
749
|
+
):
|
|
750
|
+
out.append((f"Reasoning {self.model_result.reasoning_label}", "plain"))
|
|
751
|
+
elif self.cfg is not None and self.cfg.execution.backend == "native":
|
|
752
|
+
try:
|
|
753
|
+
from ...profiles import active_subscription_selection_ready, get_active_profile
|
|
754
|
+
|
|
755
|
+
active = get_active_profile(self.cfg)
|
|
756
|
+
selection_ready = active_subscription_selection_ready(self.cfg)
|
|
757
|
+
except Exception:
|
|
758
|
+
active = None
|
|
759
|
+
selection_ready = True
|
|
760
|
+
if active is not None and active.auth_provider and not selection_ready:
|
|
761
|
+
out.append(("Model choose in /config → Default Model", "warn"))
|
|
762
|
+
out.append(("Reasoning choose with the model in /config", "warn"))
|
|
763
|
+
else:
|
|
764
|
+
out.append((f"Model {self.cfg.model}", "plain"))
|
|
765
|
+
if active is not None and active.reasoning_effort is not None:
|
|
766
|
+
out.append((f"Reasoning {active.reasoning_effort}", "plain"))
|
|
767
|
+
if self.workspace_result is not None:
|
|
768
|
+
out.append((f"Workspace {self.workspace_result.workspace}", "plain"))
|
|
769
|
+
if self.sandbox_result is not None:
|
|
770
|
+
out.append((f"Sandbox {self._sandbox_summary()}", self._sandbox_tone()))
|
|
771
|
+
if self.login_summary:
|
|
772
|
+
out.append((f"Account {self.login_summary}", "ok" if self.login_ok else "warn"))
|
|
773
|
+
for issue in self.diagnostic_lines:
|
|
774
|
+
out.append((f"Diagnostic: {issue}", "warn"))
|
|
775
|
+
out.append(("", "plain"))
|
|
776
|
+
if delegated and self.execution_result is not None:
|
|
777
|
+
runtime_id = str(self.execution_result.runtime or "").strip()
|
|
778
|
+
if self.runtime_auth_connected:
|
|
779
|
+
out.append(("Your AI subscription is connected.", "dim"))
|
|
780
|
+
out.append(
|
|
781
|
+
(
|
|
782
|
+
"Provider sign-in is immediate; leaving setup does not sign you out.",
|
|
783
|
+
"dim",
|
|
784
|
+
)
|
|
785
|
+
)
|
|
786
|
+
else:
|
|
787
|
+
out.append(
|
|
788
|
+
(
|
|
789
|
+
f"Next: run `alysis auth login {runtime_id}` before using this subscription.",
|
|
790
|
+
"warn",
|
|
791
|
+
)
|
|
792
|
+
)
|
|
793
|
+
if self.execution_result.auth_hint:
|
|
794
|
+
out.append((self.execution_result.auth_hint, "dim"))
|
|
795
|
+
else:
|
|
796
|
+
out.append(("Type your first message to start chatting.", "dim"))
|
|
797
|
+
return out
|
|
798
|
+
|
|
799
|
+
def _api_key_summary(self) -> str:
|
|
800
|
+
r = self.api_key_result
|
|
801
|
+
if r is None or not r.api_key:
|
|
802
|
+
return "not stored"
|
|
803
|
+
reason = (r.validation_message or "").strip()
|
|
804
|
+
if r.validation_status == "validated":
|
|
805
|
+
return "stored, validated"
|
|
806
|
+
if r.validation_status == "inconclusive":
|
|
807
|
+
return "stored, validation inconclusive" + (f" ({reason})" if reason else "")
|
|
808
|
+
if r.validation_status == "failed":
|
|
809
|
+
return "stored, but provider rejected it. Re-enter via /config."
|
|
810
|
+
if r.validation_status == "skipped":
|
|
811
|
+
return "stored, validation skipped"
|
|
812
|
+
if r.validation_status == "model_not_found":
|
|
813
|
+
return "stored, model validation failed"
|
|
814
|
+
return "stored"
|
|
815
|
+
|
|
816
|
+
def _api_key_tone(self) -> Tone:
|
|
817
|
+
r = self.api_key_result
|
|
818
|
+
if r is None or not r.api_key:
|
|
819
|
+
return "warn"
|
|
820
|
+
if r.validation_status == "validated":
|
|
821
|
+
return "ok"
|
|
822
|
+
if r.validation_status in {"failed", "model_not_found"}:
|
|
823
|
+
return "err"
|
|
824
|
+
return "warn"
|
|
825
|
+
|
|
826
|
+
def _sandbox_summary(self) -> str:
|
|
827
|
+
r = self.sandbox_result
|
|
828
|
+
if r is None:
|
|
829
|
+
return "skipped"
|
|
830
|
+
if r.ready:
|
|
831
|
+
return f"ready ({r.status})"
|
|
832
|
+
if r.status == "disabled":
|
|
833
|
+
return "off — host execution (less safe); re-enable via /config"
|
|
834
|
+
if r.status == "skipped":
|
|
835
|
+
return "skipped — run `alysis sandbox setup` to finish"
|
|
836
|
+
return f"{r.status} — run `alysis sandbox setup` to finish"
|
|
837
|
+
|
|
838
|
+
def _sandbox_tone(self) -> Tone:
|
|
839
|
+
r = self.sandbox_result
|
|
840
|
+
if r is not None and r.ready:
|
|
841
|
+
return "ok"
|
|
842
|
+
return "warn"
|
|
843
|
+
|
|
844
|
+
# ----------------------------------------------------------- navigation
|
|
845
|
+
|
|
846
|
+
def move(self, delta: int) -> None:
|
|
847
|
+
scr = self.screen()
|
|
848
|
+
if scr.mode != "list" or not scr.rows:
|
|
849
|
+
return
|
|
850
|
+
self.index = (self.index + delta) % len(scr.rows)
|
|
851
|
+
|
|
852
|
+
def choose_index(self, idx: int) -> None:
|
|
853
|
+
scr = self.screen()
|
|
854
|
+
if scr.mode != "list" or not scr.rows:
|
|
855
|
+
return
|
|
856
|
+
if 0 <= idx < len(scr.rows):
|
|
857
|
+
self.index = idx
|
|
858
|
+
self.choose(scr.rows[idx].value)
|
|
859
|
+
|
|
860
|
+
def choose_current(self) -> None:
|
|
861
|
+
scr = self.screen()
|
|
862
|
+
if scr.mode != "list" or not scr.rows:
|
|
863
|
+
return
|
|
864
|
+
self.choose(scr.rows[self.index].value)
|
|
865
|
+
|
|
866
|
+
def choose(self, value: str) -> None:
|
|
867
|
+
if self.stage == "connect_provider":
|
|
868
|
+
self._choose_connect_provider(value)
|
|
869
|
+
elif self.stage == "provider_advanced":
|
|
870
|
+
self._choose_provider(value)
|
|
871
|
+
elif self.stage == "model":
|
|
872
|
+
self._choose_model(value)
|
|
873
|
+
elif self.stage == "model_thinking":
|
|
874
|
+
self._choose_model_thinking(value)
|
|
875
|
+
elif self.stage == "sandbox_choice":
|
|
876
|
+
self._choose_sandbox(value)
|
|
877
|
+
|
|
878
|
+
def submit_input(self, text: str) -> None:
|
|
879
|
+
text = str(text)
|
|
880
|
+
if self.stage == "custom_name":
|
|
881
|
+
self._submit_custom_name(text)
|
|
882
|
+
elif self.stage == "custom_url":
|
|
883
|
+
self._submit_custom_url(text)
|
|
884
|
+
elif self.stage == "custom_headers":
|
|
885
|
+
self._submit_custom_headers(text)
|
|
886
|
+
elif self.stage == "api_key":
|
|
887
|
+
self._submit_api_key(text)
|
|
888
|
+
elif self.stage == "custom_model":
|
|
889
|
+
self._submit_custom_model(text)
|
|
890
|
+
elif self.stage == "workspace":
|
|
891
|
+
self._submit_workspace(text)
|
|
892
|
+
|
|
893
|
+
def confirm(self, yes: bool) -> None:
|
|
894
|
+
if self.stage == "cancel_confirm":
|
|
895
|
+
if yes:
|
|
896
|
+
self._finish(False)
|
|
897
|
+
else:
|
|
898
|
+
self._goto(self._resume_stage or "welcome")
|
|
899
|
+
elif self.stage == "sandbox_pull_confirm":
|
|
900
|
+
if yes:
|
|
901
|
+
self._goto("pulling_sandbox")
|
|
902
|
+
else:
|
|
903
|
+
self._sandbox_skip()
|
|
904
|
+
elif self.stage == "model_not_found_confirm":
|
|
905
|
+
if yes:
|
|
906
|
+
self._accept_unconfirmed_model()
|
|
907
|
+
else:
|
|
908
|
+
self._goto("model")
|
|
909
|
+
elif self.stage == "workspace_create_confirm":
|
|
910
|
+
if yes:
|
|
911
|
+
self._accept_workspace_path(
|
|
912
|
+
self._pending_workspace_path,
|
|
913
|
+
create_if_missing=True,
|
|
914
|
+
)
|
|
915
|
+
else:
|
|
916
|
+
self._pending_workspace_path = None
|
|
917
|
+
self._goto("workspace")
|
|
918
|
+
self._set_status(
|
|
919
|
+
"Type an existing folder, or enter a new folder and choose Create.",
|
|
920
|
+
"warn",
|
|
921
|
+
)
|
|
922
|
+
elif self.stage == "login_confirm":
|
|
923
|
+
if yes:
|
|
924
|
+
self._goto("logging_in")
|
|
925
|
+
else:
|
|
926
|
+
self.login_summary = ""
|
|
927
|
+
self._goto("complete")
|
|
928
|
+
elif self.stage == "runtime_login_confirm":
|
|
929
|
+
if yes:
|
|
930
|
+
self._goto("runtime_logging_in")
|
|
931
|
+
else:
|
|
932
|
+
self._skip_runtime_login()
|
|
933
|
+
|
|
934
|
+
def advance_message(self) -> None:
|
|
935
|
+
"""Enter on a ``message`` screen."""
|
|
936
|
+
if self.stage == "welcome":
|
|
937
|
+
self._goto("connect_provider")
|
|
938
|
+
elif self.stage == "complete":
|
|
939
|
+
self._finish(True)
|
|
940
|
+
elif self.stage == "fatal":
|
|
941
|
+
self._finish(False)
|
|
942
|
+
|
|
943
|
+
def back(self) -> None:
|
|
944
|
+
custom_profile = self.profile_result is not None and self.profile_result.preset is None
|
|
945
|
+
prev = {
|
|
946
|
+
"connect_provider": "welcome",
|
|
947
|
+
"provider_advanced": "connect_provider",
|
|
948
|
+
"custom_name": "provider_advanced",
|
|
949
|
+
"custom_url": "custom_name",
|
|
950
|
+
"custom_headers": "custom_url",
|
|
951
|
+
"api_key": "custom_headers" if custom_profile else "connect_provider",
|
|
952
|
+
"model": "api_key",
|
|
953
|
+
"custom_model": "model",
|
|
954
|
+
"model_thinking": "model",
|
|
955
|
+
"workspace": (
|
|
956
|
+
"connect_provider"
|
|
957
|
+
if self.execution_result is not None
|
|
958
|
+
and self.execution_result.backend == "delegated"
|
|
959
|
+
else (
|
|
960
|
+
"model_thinking"
|
|
961
|
+
if self.model_result is not None
|
|
962
|
+
and self.profile_result is not None
|
|
963
|
+
and _wiz._setup_reasoning_labels(
|
|
964
|
+
self.profile_result,
|
|
965
|
+
model=self.model_result.model,
|
|
966
|
+
current=self.model_result.reasoning_label,
|
|
967
|
+
)
|
|
968
|
+
!= ("auto",)
|
|
969
|
+
else "model"
|
|
970
|
+
)
|
|
971
|
+
),
|
|
972
|
+
"workspace_create_confirm": "workspace",
|
|
973
|
+
}.get(self.stage)
|
|
974
|
+
if self.stage == "welcome":
|
|
975
|
+
self.request_cancel()
|
|
976
|
+
return
|
|
977
|
+
if prev is None:
|
|
978
|
+
# On a confirm/busy/terminal screen Esc is a soft "skip back".
|
|
979
|
+
if self.stage in {"sandbox_choice", "sandbox_pull_confirm"}:
|
|
980
|
+
self._sandbox_skip()
|
|
981
|
+
return
|
|
982
|
+
if self.stage == "login_confirm":
|
|
983
|
+
self.login_summary = ""
|
|
984
|
+
self._goto("complete")
|
|
985
|
+
return
|
|
986
|
+
if self.stage == "runtime_login_confirm":
|
|
987
|
+
self._skip_runtime_login()
|
|
988
|
+
return
|
|
989
|
+
if self.stage == "model_not_found_confirm":
|
|
990
|
+
self._goto("model")
|
|
991
|
+
return
|
|
992
|
+
return
|
|
993
|
+
self._set_status("", "dim")
|
|
994
|
+
self._goto(prev)
|
|
995
|
+
|
|
996
|
+
def request_cancel(self) -> None:
|
|
997
|
+
"""Ctrl+C / Esc-at-welcome: confirm before discarding."""
|
|
998
|
+
if self.stage in {"cancel_confirm", "complete", "fatal", "done"}:
|
|
999
|
+
return
|
|
1000
|
+
self._resume_stage = self.stage
|
|
1001
|
+
self._goto("cancel_confirm", keep_status=True)
|
|
1002
|
+
|
|
1003
|
+
# --------------------------------------------------------- stage helpers
|
|
1004
|
+
|
|
1005
|
+
def _goto(self, stage: str, *, reset_index: bool = True, keep_status: bool = False) -> None:
|
|
1006
|
+
self.stage = stage
|
|
1007
|
+
if reset_index:
|
|
1008
|
+
self.index = 0
|
|
1009
|
+
if not keep_status:
|
|
1010
|
+
self._set_status("", "dim")
|
|
1011
|
+
|
|
1012
|
+
def _finish(self, success: bool) -> None:
|
|
1013
|
+
self.success = success
|
|
1014
|
+
self.stage = "done"
|
|
1015
|
+
|
|
1016
|
+
# --------------------------------------------------------- provider step
|
|
1017
|
+
|
|
1018
|
+
def _choose_connect_provider(self, value: str) -> None:
|
|
1019
|
+
"""Dispatch a merged-picker row to the right branch.
|
|
1020
|
+
|
|
1021
|
+
Rows are either a subscription (``runtime:<id>`` → workspace, OAuth
|
|
1022
|
+
happens post-commit), the advanced sub-list, or an API-key/local
|
|
1023
|
+
provider preset (→ API key step). Switching between a subscription and
|
|
1024
|
+
a provider — or between different subscriptions — invalidates the
|
|
1025
|
+
dependent step results, mirroring the old method-first reset.
|
|
1026
|
+
"""
|
|
1027
|
+
if value == _wiz._ADVANCED_PROVIDER_PRESETS_VALUE:
|
|
1028
|
+
self._goto("provider_advanced")
|
|
1029
|
+
return
|
|
1030
|
+
if value.startswith(_wiz._RUNTIME_EXECUTION_PREFIX):
|
|
1031
|
+
try:
|
|
1032
|
+
selected = _wiz._execution_result_from_value(value)
|
|
1033
|
+
except ConfigError as exc:
|
|
1034
|
+
self._set_status(str(exc), "err")
|
|
1035
|
+
return
|
|
1036
|
+
if self.execution_result is not None and selected != self.execution_result:
|
|
1037
|
+
self.profile_result = None
|
|
1038
|
+
self.api_key_result = None
|
|
1039
|
+
self.model_result = None
|
|
1040
|
+
self._discovered_model_options = ()
|
|
1041
|
+
self._model_catalog_warning = ""
|
|
1042
|
+
self.execution_result = selected
|
|
1043
|
+
self._goto("workspace")
|
|
1044
|
+
return
|
|
1045
|
+
selected = _ExecutionStepResult(backend="native")
|
|
1046
|
+
if self.execution_result is not None and selected != self.execution_result:
|
|
1047
|
+
self.profile_result = None
|
|
1048
|
+
self.api_key_result = None
|
|
1049
|
+
self.model_result = None
|
|
1050
|
+
self._discovered_model_options = ()
|
|
1051
|
+
self._model_catalog_warning = ""
|
|
1052
|
+
self.execution_result = selected
|
|
1053
|
+
self._choose_provider(value)
|
|
1054
|
+
|
|
1055
|
+
def _choose_provider(self, key: str) -> None:
|
|
1056
|
+
if key == _wiz._ADVANCED_PROVIDER_PRESETS_VALUE:
|
|
1057
|
+
self._goto("provider_advanced")
|
|
1058
|
+
return
|
|
1059
|
+
preset = _wiz._preset_by_key(key)
|
|
1060
|
+
if preset is None:
|
|
1061
|
+
self._set_status(f"Unknown provider preset: {key}", "err")
|
|
1062
|
+
return
|
|
1063
|
+
if preset.key == _wiz._CUSTOM_PROVIDER_KEY:
|
|
1064
|
+
self._goto("custom_name")
|
|
1065
|
+
return
|
|
1066
|
+
profile = make_profile_from_preset(preset)
|
|
1067
|
+
new = _ProfileStepResult(profile=profile, label=preset.label, preset=preset)
|
|
1068
|
+
# Re-selecting a different provider invalidates the dependent steps.
|
|
1069
|
+
if self.profile_result is not None and new != self.profile_result:
|
|
1070
|
+
self.api_key_result = None
|
|
1071
|
+
self.model_result = None
|
|
1072
|
+
self._discovered_model_options = ()
|
|
1073
|
+
self._model_catalog_warning = ""
|
|
1074
|
+
self.profile_result = new
|
|
1075
|
+
warning = (preset.setup_warning or "").strip()
|
|
1076
|
+
if self._hosted_mimo():
|
|
1077
|
+
# The Alysis Code account needs no API key and login picks the
|
|
1078
|
+
# hosted default model — skip both steps and head straight to the
|
|
1079
|
+
# workspace; the post-commit login_confirm step connects the
|
|
1080
|
+
# account in the browser.
|
|
1081
|
+
self.api_key_result = _ApiKeyStepResult(
|
|
1082
|
+
api_key="",
|
|
1083
|
+
validation_status="skipped",
|
|
1084
|
+
validation_message="Alysis Code account uses browser sign-in — no API key.",
|
|
1085
|
+
)
|
|
1086
|
+
self.model_result = _ModelStepResult(model="deepseek-v4-flash")
|
|
1087
|
+
self._goto("workspace")
|
|
1088
|
+
self._set_status(
|
|
1089
|
+
"Alysis Code account: no API key needed — you'll sign in after setup.",
|
|
1090
|
+
"ok",
|
|
1091
|
+
)
|
|
1092
|
+
return
|
|
1093
|
+
self._goto("api_key")
|
|
1094
|
+
if warning:
|
|
1095
|
+
self._set_status(warning, "warn")
|
|
1096
|
+
|
|
1097
|
+
def _submit_custom_name(self, text: str) -> None:
|
|
1098
|
+
name = text.strip().lower() or "custom"
|
|
1099
|
+
self._custom_name = name
|
|
1100
|
+
self._goto("custom_url")
|
|
1101
|
+
|
|
1102
|
+
def _submit_custom_url(self, text: str) -> None:
|
|
1103
|
+
url = text.strip()
|
|
1104
|
+
if not url:
|
|
1105
|
+
self._set_status("Base URL is required.", "err")
|
|
1106
|
+
return
|
|
1107
|
+
self._custom_url = url
|
|
1108
|
+
self._goto("custom_headers")
|
|
1109
|
+
|
|
1110
|
+
def _submit_custom_headers(self, text: str) -> None:
|
|
1111
|
+
headers, ok = self._parse_extra_headers(text)
|
|
1112
|
+
if not ok:
|
|
1113
|
+
self._set_status("Extra headers must use k=v syntax.", "err")
|
|
1114
|
+
return
|
|
1115
|
+
self._custom_headers = headers
|
|
1116
|
+
profile = ProfileSpec(
|
|
1117
|
+
name=self._custom_name,
|
|
1118
|
+
protocol="openai_compat",
|
|
1119
|
+
base_url=self._custom_url,
|
|
1120
|
+
extra_headers=headers,
|
|
1121
|
+
notes="Custom OpenAI-compatible endpoint.",
|
|
1122
|
+
)
|
|
1123
|
+
new = _ProfileStepResult(profile=profile, label=profile.name, preset=None)
|
|
1124
|
+
if self.profile_result is not None and new != self.profile_result:
|
|
1125
|
+
self.api_key_result = None
|
|
1126
|
+
self.model_result = None
|
|
1127
|
+
self._discovered_model_options = ()
|
|
1128
|
+
self._model_catalog_warning = ""
|
|
1129
|
+
self.profile_result = new
|
|
1130
|
+
self._goto("api_key")
|
|
1131
|
+
|
|
1132
|
+
@staticmethod
|
|
1133
|
+
def _parse_extra_headers(text: str) -> tuple[dict[str, str], bool]:
|
|
1134
|
+
"""Parse ``k=v, k=v`` into a header dict (mirrors the classic wizard).
|
|
1135
|
+
|
|
1136
|
+
Returns ``(headers, ok)``; ``ok`` is False when an item lacks ``=`` so
|
|
1137
|
+
the caller can re-prompt. An empty string yields ``({}, True)``.
|
|
1138
|
+
"""
|
|
1139
|
+
headers: dict[str, str] = {}
|
|
1140
|
+
for item in str(text).split(","):
|
|
1141
|
+
piece = item.strip()
|
|
1142
|
+
if not piece:
|
|
1143
|
+
continue
|
|
1144
|
+
if "=" not in piece:
|
|
1145
|
+
return {}, False
|
|
1146
|
+
key, value = piece.split("=", 1)
|
|
1147
|
+
if key.strip() and value.strip():
|
|
1148
|
+
headers[key.strip()] = value.strip()
|
|
1149
|
+
return headers, True
|
|
1150
|
+
|
|
1151
|
+
# --------------------------------------------------------- api key step
|
|
1152
|
+
|
|
1153
|
+
def _submit_api_key(self, text: str) -> None:
|
|
1154
|
+
profile = self.profile_result.profile # type: ignore[union-attr]
|
|
1155
|
+
value = text.strip()
|
|
1156
|
+
required = bool(profile.api_key_env)
|
|
1157
|
+
has_key = self.api_key_result is not None and bool(self.api_key_result.api_key)
|
|
1158
|
+
if not value:
|
|
1159
|
+
if has_key:
|
|
1160
|
+
# Returning to this step (e.g. Esc from model) keeps the already
|
|
1161
|
+
# validated key instead of forcing a re-paste — matches the
|
|
1162
|
+
# classic wizard's "Enter to keep current".
|
|
1163
|
+
self._goto("model")
|
|
1164
|
+
return
|
|
1165
|
+
if required:
|
|
1166
|
+
# Empty has no cap of its own (it just re-prompts), so it must not
|
|
1167
|
+
# consume the genuine validation-failure budget (_key_attempts).
|
|
1168
|
+
self._set_status("API key is required to continue.", "err")
|
|
1169
|
+
return
|
|
1170
|
+
self.api_key_result = _ApiKeyStepResult(
|
|
1171
|
+
api_key="", validation_status="skipped", validation_message="No API key provided."
|
|
1172
|
+
)
|
|
1173
|
+
self._goto("model")
|
|
1174
|
+
return
|
|
1175
|
+
# Stash the candidate and validate on the worker.
|
|
1176
|
+
self._pending_key = value
|
|
1177
|
+
self._goto("validating_key")
|
|
1178
|
+
|
|
1179
|
+
def _run_validating_key(self) -> None:
|
|
1180
|
+
value = getattr(self, "_pending_key", "")
|
|
1181
|
+
self._report("Validating key…")
|
|
1182
|
+
validation = _wiz._validate_api_key(
|
|
1183
|
+
profile=self.profile_result.profile, # type: ignore[union-attr]
|
|
1184
|
+
api_key=value,
|
|
1185
|
+
suggested_models=_wiz._suggested_models(self.profile_result), # type: ignore[arg-type]
|
|
1186
|
+
validation_model=_wiz._validation_model_hint(self.profile_result), # type: ignore[arg-type]
|
|
1187
|
+
)
|
|
1188
|
+
status = validation.status
|
|
1189
|
+
message = validation.message
|
|
1190
|
+
if status == "validated":
|
|
1191
|
+
self.api_key_result = _ApiKeyStepResult(
|
|
1192
|
+
api_key=value, validation_status="validated", validation_message=message
|
|
1193
|
+
)
|
|
1194
|
+
self._load_provider_model_catalog(value)
|
|
1195
|
+
self._goto("model")
|
|
1196
|
+
if message:
|
|
1197
|
+
self._set_status(message, "warn")
|
|
1198
|
+
return
|
|
1199
|
+
if status in {"inconclusive", "model_not_found"}:
|
|
1200
|
+
self.api_key_result = _ApiKeyStepResult(
|
|
1201
|
+
api_key=value, validation_status=status, validation_message=message
|
|
1202
|
+
)
|
|
1203
|
+
self._load_provider_model_catalog(value)
|
|
1204
|
+
self._goto("model")
|
|
1205
|
+
self._set_status(
|
|
1206
|
+
(message or "")
|
|
1207
|
+
+ (
|
|
1208
|
+
" We'll verify your chosen model next."
|
|
1209
|
+
if status == "model_not_found"
|
|
1210
|
+
else " Continuing without validation."
|
|
1211
|
+
),
|
|
1212
|
+
"warn",
|
|
1213
|
+
)
|
|
1214
|
+
return
|
|
1215
|
+
# Failed: allow a few retries, then continue with the last key.
|
|
1216
|
+
self._key_attempts += 1
|
|
1217
|
+
if self._key_attempts >= _MAX_KEY_ATTEMPTS:
|
|
1218
|
+
self.api_key_result = _ApiKeyStepResult(
|
|
1219
|
+
api_key=value, validation_status="failed", validation_message=message
|
|
1220
|
+
)
|
|
1221
|
+
self._discovered_model_options = ()
|
|
1222
|
+
self._model_catalog_warning = (
|
|
1223
|
+
"Live model discovery was skipped because the provider rejected the API key."
|
|
1224
|
+
)
|
|
1225
|
+
self._goto("model")
|
|
1226
|
+
self._set_status("Continuing with the last key. Fix it later in /config.", "warn")
|
|
1227
|
+
return
|
|
1228
|
+
self._goto("api_key")
|
|
1229
|
+
self._set_status(
|
|
1230
|
+
f"Key validation failed: {message or 'provider rejected the key'} (attempt {self._key_attempts}/{_MAX_KEY_ATTEMPTS})",
|
|
1231
|
+
"err",
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
def _load_provider_model_catalog(self, api_key: str) -> None:
|
|
1235
|
+
options, warning = _wiz._discover_setup_provider_models( # type: ignore[arg-type]
|
|
1236
|
+
self.profile_result,
|
|
1237
|
+
api_key=api_key,
|
|
1238
|
+
)
|
|
1239
|
+
self._discovered_model_options = options
|
|
1240
|
+
self._model_catalog_warning = warning
|
|
1241
|
+
|
|
1242
|
+
# ----------------------------------------------------------- model step
|
|
1243
|
+
|
|
1244
|
+
def _choose_model(self, value: str) -> None:
|
|
1245
|
+
if value == _wiz._CUSTOM_MODEL_VALUE:
|
|
1246
|
+
self._goto("custom_model")
|
|
1247
|
+
return
|
|
1248
|
+
self._set_model(value, custom=False)
|
|
1249
|
+
|
|
1250
|
+
def _submit_custom_model(self, text: str) -> None:
|
|
1251
|
+
model = text.strip()
|
|
1252
|
+
if not model:
|
|
1253
|
+
self._set_status("Model is required.", "err")
|
|
1254
|
+
return
|
|
1255
|
+
self._set_model(model, custom=True)
|
|
1256
|
+
|
|
1257
|
+
def _set_model(self, model: str, *, custom: bool) -> None:
|
|
1258
|
+
preset = self.profile_result.preset if self.profile_result else None
|
|
1259
|
+
if preset is not None:
|
|
1260
|
+
model = canonical_model_alias_for_preset(preset, model)
|
|
1261
|
+
model = model.strip()
|
|
1262
|
+
if not model:
|
|
1263
|
+
self._set_status("Default model is required.", "err")
|
|
1264
|
+
return
|
|
1265
|
+
self.model_result = _ModelStepResult(model=model, custom=custom)
|
|
1266
|
+
self._goto("validating_model")
|
|
1267
|
+
|
|
1268
|
+
def _advance_after_model_validation(self) -> None:
|
|
1269
|
+
if self.model_result is None or self.profile_result is None:
|
|
1270
|
+
self._goto("workspace")
|
|
1271
|
+
return
|
|
1272
|
+
labels = _wiz._setup_reasoning_labels(
|
|
1273
|
+
self.profile_result,
|
|
1274
|
+
model=self.model_result.model,
|
|
1275
|
+
current=self.model_result.reasoning_label,
|
|
1276
|
+
)
|
|
1277
|
+
if labels == ("auto",):
|
|
1278
|
+
self.model_result = replace(self.model_result, reasoning_label="auto")
|
|
1279
|
+
self._goto("workspace")
|
|
1280
|
+
preset = self.profile_result.preset
|
|
1281
|
+
if preset is not None and preset.key == _wiz._NVIDIA_PRESET_KEY:
|
|
1282
|
+
self._set_status(
|
|
1283
|
+
"No verified reasoning control is known for this NVIDIA-hosted model; "
|
|
1284
|
+
"the provider default will be used.",
|
|
1285
|
+
"warn",
|
|
1286
|
+
)
|
|
1287
|
+
return
|
|
1288
|
+
self._goto("model_thinking")
|
|
1289
|
+
|
|
1290
|
+
def _choose_model_thinking(self, value: str) -> None:
|
|
1291
|
+
if self.model_result is None:
|
|
1292
|
+
self._goto("model")
|
|
1293
|
+
return
|
|
1294
|
+
labels = _wiz._setup_reasoning_labels( # type: ignore[arg-type]
|
|
1295
|
+
self.profile_result,
|
|
1296
|
+
model=self.model_result.model,
|
|
1297
|
+
current=self.model_result.reasoning_label,
|
|
1298
|
+
)
|
|
1299
|
+
if value not in labels:
|
|
1300
|
+
self._set_status("That reasoning effort is not supported by this model.", "err")
|
|
1301
|
+
return
|
|
1302
|
+
self.model_result = replace(self.model_result, reasoning_label=value)
|
|
1303
|
+
self._goto("workspace")
|
|
1304
|
+
|
|
1305
|
+
def _run_validating_model(self) -> None:
|
|
1306
|
+
key = self.api_key_result
|
|
1307
|
+
if key is None or not key.api_key or key.validation_status == "failed":
|
|
1308
|
+
# Nothing to validate against; accept the model and move on.
|
|
1309
|
+
self._advance_after_model_validation()
|
|
1310
|
+
return
|
|
1311
|
+
self._report("Validating model…")
|
|
1312
|
+
validation = _wiz._validate_api_key(
|
|
1313
|
+
profile=self.profile_result.profile, # type: ignore[union-attr]
|
|
1314
|
+
api_key=key.api_key,
|
|
1315
|
+
model=self.model_result.model, # type: ignore[union-attr]
|
|
1316
|
+
suggested_models=_wiz._suggested_models(self.profile_result), # type: ignore[arg-type]
|
|
1317
|
+
)
|
|
1318
|
+
self.api_key_result = replace(
|
|
1319
|
+
key, validation_status=validation.status, validation_message=validation.message
|
|
1320
|
+
)
|
|
1321
|
+
if validation.status == "model_not_found":
|
|
1322
|
+
self._last_model_not_found = self.model_result.model # type: ignore[union-attr]
|
|
1323
|
+
if self.model_result is not None and self.model_result.custom:
|
|
1324
|
+
self._goto("model_not_found_confirm")
|
|
1325
|
+
else:
|
|
1326
|
+
self._goto("model")
|
|
1327
|
+
self._set_status(validation.message or "Model not found at this provider.", "err")
|
|
1328
|
+
return
|
|
1329
|
+
if validation.status == "validated":
|
|
1330
|
+
self._advance_after_model_validation()
|
|
1331
|
+
self._set_status("Model validated.", "ok")
|
|
1332
|
+
return
|
|
1333
|
+
if validation.status == "failed":
|
|
1334
|
+
self._advance_after_model_validation()
|
|
1335
|
+
self._set_status(validation.message or "Provider rejected the key.", "err")
|
|
1336
|
+
return
|
|
1337
|
+
self._advance_after_model_validation()
|
|
1338
|
+
if validation.message:
|
|
1339
|
+
self._set_status(validation.message, "warn")
|
|
1340
|
+
|
|
1341
|
+
def _accept_unconfirmed_model(self) -> None:
|
|
1342
|
+
warning = (
|
|
1343
|
+
f"Model '{self._last_model_not_found}' was not confirmed; "
|
|
1344
|
+
"provider reported it missing and you chose to use it anyway."
|
|
1345
|
+
)
|
|
1346
|
+
if self.api_key_result is not None:
|
|
1347
|
+
self.api_key_result = replace(
|
|
1348
|
+
self.api_key_result, validation_status="inconclusive", validation_message=warning
|
|
1349
|
+
)
|
|
1350
|
+
self._advance_after_model_validation()
|
|
1351
|
+
self._set_status(warning, "warn")
|
|
1352
|
+
|
|
1353
|
+
# ------------------------------------------------------- workspace step
|
|
1354
|
+
|
|
1355
|
+
def _submit_workspace(self, text: str) -> None:
|
|
1356
|
+
default = _wiz._suggest_workspace_default()
|
|
1357
|
+
raw = text.strip() or os.fspath(default)
|
|
1358
|
+
selected = Path(raw).expanduser().resolve(strict=False)
|
|
1359
|
+
if not selected.exists():
|
|
1360
|
+
self._pending_workspace_path = selected
|
|
1361
|
+
self._goto("workspace_create_confirm")
|
|
1362
|
+
return
|
|
1363
|
+
self._accept_workspace_path(selected, create_if_missing=False)
|
|
1364
|
+
|
|
1365
|
+
def _accept_workspace_path(
|
|
1366
|
+
self,
|
|
1367
|
+
selected: Path | None,
|
|
1368
|
+
*,
|
|
1369
|
+
create_if_missing: bool,
|
|
1370
|
+
) -> None:
|
|
1371
|
+
if selected is None:
|
|
1372
|
+
self._goto("workspace")
|
|
1373
|
+
self._set_status("Choose a workspace folder.", "err")
|
|
1374
|
+
return
|
|
1375
|
+
try:
|
|
1376
|
+
binding = resolve_workspace_binding(
|
|
1377
|
+
selected,
|
|
1378
|
+
create_if_missing=create_if_missing,
|
|
1379
|
+
allow_broad_workspace=True,
|
|
1380
|
+
source="setup_wizard",
|
|
1381
|
+
)
|
|
1382
|
+
except WorkspaceBindingError as exc:
|
|
1383
|
+
self._goto("workspace")
|
|
1384
|
+
self._set_status(self._workspace_error_message(selected, exc), "err")
|
|
1385
|
+
return
|
|
1386
|
+
self.workspace_result = _WorkspaceStepResult(workspace=os.fspath(binding.requested_path))
|
|
1387
|
+
self._pending_workspace_path = None
|
|
1388
|
+
self._goto("committing")
|
|
1389
|
+
|
|
1390
|
+
@staticmethod
|
|
1391
|
+
def _workspace_error_message(selected: Path, exc: WorkspaceBindingError) -> str:
|
|
1392
|
+
message = str(exc)
|
|
1393
|
+
if "create_if_missing=True" in message:
|
|
1394
|
+
return f"Folder does not exist: {selected}. Type an existing folder or choose Create."
|
|
1395
|
+
if "not a directory" in message:
|
|
1396
|
+
return f"That path is not a folder: {selected}"
|
|
1397
|
+
return message
|
|
1398
|
+
|
|
1399
|
+
# ---------------------------------------------------------- commit step
|
|
1400
|
+
|
|
1401
|
+
def _run_committing(self) -> None:
|
|
1402
|
+
self._report("Saving setup…")
|
|
1403
|
+
sink = _RichConsole(file=io.StringIO(), force_terminal=False, no_color=True)
|
|
1404
|
+
try:
|
|
1405
|
+
if self.execution_result is not None and self.execution_result.backend == "delegated":
|
|
1406
|
+
cfg = _wiz._commit_delegated_setup(
|
|
1407
|
+
execution_result=self.execution_result,
|
|
1408
|
+
workspace_result=self.workspace_result, # type: ignore[arg-type]
|
|
1409
|
+
console=sink,
|
|
1410
|
+
)
|
|
1411
|
+
else:
|
|
1412
|
+
cfg = _wiz._commit_setup(
|
|
1413
|
+
profile_result=self.profile_result, # type: ignore[arg-type]
|
|
1414
|
+
api_key_result=self.api_key_result, # type: ignore[arg-type]
|
|
1415
|
+
model_result=self.model_result, # type: ignore[arg-type]
|
|
1416
|
+
workspace_result=self.workspace_result, # type: ignore[arg-type]
|
|
1417
|
+
console=sink,
|
|
1418
|
+
)
|
|
1419
|
+
except (ConfigError, OSError) as exc:
|
|
1420
|
+
self.fatal_error = f"Failed to save setup: {exc}"
|
|
1421
|
+
self._goto("fatal")
|
|
1422
|
+
return
|
|
1423
|
+
self.cfg = cfg
|
|
1424
|
+
if self.execution_result is not None and self.execution_result.backend == "delegated":
|
|
1425
|
+
self.diagnostic_lines = []
|
|
1426
|
+
self.sandbox_result = None
|
|
1427
|
+
self._goto("checking_runtime_auth")
|
|
1428
|
+
return
|
|
1429
|
+
self.diagnostic_lines = self._collect_diagnostics(cfg)
|
|
1430
|
+
self._goto("diagnosing_sandbox")
|
|
1431
|
+
|
|
1432
|
+
def _collect_diagnostics(self, cfg: AppConfig) -> list[str]:
|
|
1433
|
+
try:
|
|
1434
|
+
from ...provider_diagnostics import provider_diagnostic_warning_lines
|
|
1435
|
+
|
|
1436
|
+
return list(provider_diagnostic_warning_lines(cfg))
|
|
1437
|
+
except Exception:
|
|
1438
|
+
return []
|
|
1439
|
+
|
|
1440
|
+
# ------------------------------------------------ runtime account step
|
|
1441
|
+
|
|
1442
|
+
def _run_checking_runtime_auth(self) -> None:
|
|
1443
|
+
self._report("Checking subscription connection…")
|
|
1444
|
+
result: _DelegatedAuthResult = _wiz._check_delegated_runtime_connection(
|
|
1445
|
+
self.cfg, # type: ignore[arg-type]
|
|
1446
|
+
self.execution_result, # type: ignore[arg-type]
|
|
1447
|
+
)
|
|
1448
|
+
self.runtime_auth_connected = result.connected
|
|
1449
|
+
self.runtime_auth_summary = result.summary
|
|
1450
|
+
if result.connected and self.cfg is not None and self.cfg.execution.backend == "native":
|
|
1451
|
+
try:
|
|
1452
|
+
_wiz._sync_direct_subscription_model(
|
|
1453
|
+
self.cfg,
|
|
1454
|
+
str(self.execution_result.runtime or ""), # type: ignore[union-attr]
|
|
1455
|
+
)
|
|
1456
|
+
except Exception as exc: # noqa: BLE001
|
|
1457
|
+
self._goto("runtime_login_confirm")
|
|
1458
|
+
self._set_status(f"Connected, but model discovery failed: {exc}", "err")
|
|
1459
|
+
return
|
|
1460
|
+
self.diagnostic_lines = self._collect_diagnostics(self.cfg)
|
|
1461
|
+
self._goto("diagnosing_sandbox")
|
|
1462
|
+
return
|
|
1463
|
+
self._goto("complete" if result.connected else "runtime_login_confirm")
|
|
1464
|
+
|
|
1465
|
+
def _skip_runtime_login(self) -> None:
|
|
1466
|
+
self.runtime_auth_connected = False
|
|
1467
|
+
if not self.runtime_auth_summary:
|
|
1468
|
+
self.runtime_auth_summary = "not connected"
|
|
1469
|
+
self._goto("complete")
|
|
1470
|
+
|
|
1471
|
+
def _run_runtime_logging_in(self) -> None:
|
|
1472
|
+
self._report("Opening provider sign-in…")
|
|
1473
|
+
result: _DelegatedAuthResult = _wiz._login_delegated_runtime(
|
|
1474
|
+
self.cfg, # type: ignore[arg-type]
|
|
1475
|
+
self.execution_result, # type: ignore[arg-type]
|
|
1476
|
+
)
|
|
1477
|
+
self.runtime_auth_connected = result.connected
|
|
1478
|
+
self.runtime_auth_summary = result.summary
|
|
1479
|
+
if result.connected and self.cfg is not None and self.cfg.execution.backend == "native":
|
|
1480
|
+
self.diagnostic_lines = self._collect_diagnostics(self.cfg)
|
|
1481
|
+
self._goto("diagnosing_sandbox")
|
|
1482
|
+
else:
|
|
1483
|
+
self._goto("complete")
|
|
1484
|
+
|
|
1485
|
+
# --------------------------------------------------------- sandbox step
|
|
1486
|
+
|
|
1487
|
+
def _run_diagnosing_sandbox(self) -> None:
|
|
1488
|
+
from ...sandbox_doctor import detect_bubblewrap_install_plan, diagnose_sandbox
|
|
1489
|
+
|
|
1490
|
+
self._report("Checking sandbox readiness…")
|
|
1491
|
+
try:
|
|
1492
|
+
result = diagnose_sandbox(self.cfg, include_smoke=False)
|
|
1493
|
+
except Exception as exc: # noqa: BLE001 - backends fail in environment-specific ways
|
|
1494
|
+
self.sandbox_result = _SandboxStepResult(ready=False, status="check failed")
|
|
1495
|
+
self._after_sandbox()
|
|
1496
|
+
self._set_status(f"Sandbox check failed: {exc}", "warn")
|
|
1497
|
+
return
|
|
1498
|
+
self._sandbox_diag = result
|
|
1499
|
+
if result.ready:
|
|
1500
|
+
self.sandbox_result = _SandboxStepResult(
|
|
1501
|
+
ready=True, status=result.selected_backend or result.status
|
|
1502
|
+
)
|
|
1503
|
+
self._after_sandbox()
|
|
1504
|
+
return
|
|
1505
|
+
if result.can_pull:
|
|
1506
|
+
self._goto("sandbox_pull_confirm")
|
|
1507
|
+
return
|
|
1508
|
+
self._sandbox_plan = detect_bubblewrap_install_plan()
|
|
1509
|
+
self._goto("sandbox_choice")
|
|
1510
|
+
|
|
1511
|
+
def _choose_sandbox(self, value: str) -> None:
|
|
1512
|
+
if value == "install_bwrap":
|
|
1513
|
+
self._goto("installing_sandbox")
|
|
1514
|
+
elif value == "disable":
|
|
1515
|
+
self._goto("disabling_sandbox")
|
|
1516
|
+
else:
|
|
1517
|
+
self._sandbox_skip()
|
|
1518
|
+
|
|
1519
|
+
def _sandbox_skip(self) -> None:
|
|
1520
|
+
self.sandbox_result = _SandboxStepResult(ready=False, status="not ready")
|
|
1521
|
+
self._after_sandbox()
|
|
1522
|
+
|
|
1523
|
+
def _run_disabling_sandbox(self) -> None:
|
|
1524
|
+
from ...config import save_config
|
|
1525
|
+
from ...sandbox_settings import apply_sandbox_mode_to_config
|
|
1526
|
+
|
|
1527
|
+
apply_sandbox_mode_to_config(self.cfg, "off")
|
|
1528
|
+
try:
|
|
1529
|
+
save_config(self.cfg)
|
|
1530
|
+
self.sandbox_result = _SandboxStepResult(ready=False, status="disabled")
|
|
1531
|
+
except (ConfigError, OSError) as exc:
|
|
1532
|
+
self.sandbox_result = _SandboxStepResult(ready=False, status="not ready")
|
|
1533
|
+
self._set_status(f"Could not save the sandbox setting: {exc}", "warn")
|
|
1534
|
+
self._after_sandbox()
|
|
1535
|
+
|
|
1536
|
+
def _run_installing_sandbox(self) -> None:
|
|
1537
|
+
from ...sandbox_doctor import diagnose_sandbox, install_bubblewrap
|
|
1538
|
+
|
|
1539
|
+
self._report("Installing bubblewrap…")
|
|
1540
|
+
install = install_bubblewrap(plan=self._sandbox_plan)
|
|
1541
|
+
if not install.ok:
|
|
1542
|
+
self.sandbox_result = _SandboxStepResult(ready=False, status="not ready")
|
|
1543
|
+
self._after_sandbox()
|
|
1544
|
+
self._set_status(f"Bubblewrap install did not complete: {install.detail}", "warn")
|
|
1545
|
+
return
|
|
1546
|
+
self._recheck_sandbox(diagnose_sandbox)
|
|
1547
|
+
|
|
1548
|
+
def _run_pulling_sandbox(self) -> None:
|
|
1549
|
+
from ...sandbox_doctor import diagnose_sandbox, pull_sandbox_images
|
|
1550
|
+
|
|
1551
|
+
self._report("Pulling sandbox image…")
|
|
1552
|
+
try:
|
|
1553
|
+
pull = pull_sandbox_images(timeout_s=900)
|
|
1554
|
+
except Exception as exc: # noqa: BLE001
|
|
1555
|
+
self.sandbox_result = _SandboxStepResult(ready=False, status="pull failed")
|
|
1556
|
+
self._after_sandbox()
|
|
1557
|
+
self._set_status(f"Sandbox image pull failed: {exc}", "warn")
|
|
1558
|
+
return
|
|
1559
|
+
if not pull.ok:
|
|
1560
|
+
self.sandbox_result = _SandboxStepResult(ready=False, status="pull failed")
|
|
1561
|
+
self._after_sandbox()
|
|
1562
|
+
self._set_status(
|
|
1563
|
+
f"Sandbox image pull failed: {pull.error or 'image pull failed'}", "warn"
|
|
1564
|
+
)
|
|
1565
|
+
return
|
|
1566
|
+
self._recheck_sandbox(diagnose_sandbox)
|
|
1567
|
+
|
|
1568
|
+
def _recheck_sandbox(self, diagnose_sandbox: Callable[..., Any]) -> None:
|
|
1569
|
+
try:
|
|
1570
|
+
result = diagnose_sandbox(self.cfg, include_smoke=True)
|
|
1571
|
+
except Exception as exc: # noqa: BLE001
|
|
1572
|
+
self.sandbox_result = _SandboxStepResult(ready=False, status="check failed")
|
|
1573
|
+
self._after_sandbox()
|
|
1574
|
+
self._set_status(f"Sandbox check failed after setup: {exc}", "warn")
|
|
1575
|
+
return
|
|
1576
|
+
if result.ready:
|
|
1577
|
+
self.sandbox_result = _SandboxStepResult(
|
|
1578
|
+
ready=True, status=result.selected_backend or result.status
|
|
1579
|
+
)
|
|
1580
|
+
else:
|
|
1581
|
+
self.sandbox_result = _SandboxStepResult(ready=False, status="not ready")
|
|
1582
|
+
self._after_sandbox()
|
|
1583
|
+
|
|
1584
|
+
def _after_sandbox(self) -> None:
|
|
1585
|
+
if self._hosted_mimo():
|
|
1586
|
+
self._goto("login_confirm")
|
|
1587
|
+
else:
|
|
1588
|
+
self._goto("complete")
|
|
1589
|
+
|
|
1590
|
+
# ----------------------------------------------------------- login step
|
|
1591
|
+
|
|
1592
|
+
def _run_logging_in(self) -> None:
|
|
1593
|
+
from ... import account_login
|
|
1594
|
+
|
|
1595
|
+
self._report("Connecting…")
|
|
1596
|
+
captured: list[str] = []
|
|
1597
|
+
try:
|
|
1598
|
+
result = account_login.login(self.cfg, output_write=lambda m: captured.append(str(m)))
|
|
1599
|
+
except Exception as exc: # noqa: BLE001 - login failures are non-fatal here
|
|
1600
|
+
# Non-fatal: setup is already saved; the summary row (in warn tone)
|
|
1601
|
+
# carries the message, so we don't also raise a redundant status line.
|
|
1602
|
+
self.login_ok = False
|
|
1603
|
+
self.login_summary = f"not connected ({exc}). Run `alysis login` later."
|
|
1604
|
+
self._goto("complete")
|
|
1605
|
+
return
|
|
1606
|
+
who = f" as {result.email}" if getattr(result, "email", None) else ""
|
|
1607
|
+
self.login_ok = True
|
|
1608
|
+
self.login_summary = f"connected{who} — Alysis Code account ready"
|
|
1609
|
+
self._goto("complete")
|
|
1610
|
+
|
|
1611
|
+
# ----------------------------------------------------------- busy driver
|
|
1612
|
+
|
|
1613
|
+
def run_busy(self) -> None:
|
|
1614
|
+
"""Execute the current ``busy`` stage's blocking work (worker/terminal).
|
|
1615
|
+
|
|
1616
|
+
Each handler transitions :attr:`stage` to the next screen before
|
|
1617
|
+
returning, so the application only needs to re-render afterwards.
|
|
1618
|
+
"""
|
|
1619
|
+
handler = getattr(self, f"_run_{self.stage}", None)
|
|
1620
|
+
if handler is None:
|
|
1621
|
+
return
|
|
1622
|
+
handler()
|