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,519 @@
|
|
|
1
|
+
"""Full-screen TUI front-end for the first-run setup wizard.
|
|
2
|
+
|
|
3
|
+
A prompt_toolkit ``Application`` that walks a :class:`SetupFlow` through the
|
|
4
|
+
onboarding steps using the same visual grammar as the chat TUI: the white owl on
|
|
5
|
+
the welcome/finish screens, the green ``#3fb950`` accent, the reusable picker
|
|
6
|
+
renderer, and a pinned footer. The flow owns all of the wizard *logic*; this
|
|
7
|
+
module only paints its :class:`Screen` and routes key events back into it.
|
|
8
|
+
|
|
9
|
+
Focus is held permanently on a single hidden ``TextArea`` (shown only on input
|
|
10
|
+
steps). Navigation in list/confirm/message screens runs through eager global key
|
|
11
|
+
bindings — exactly how the chat TUI's picker works — so we never juggle focus
|
|
12
|
+
across the worker thread that runs the network/validation steps.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from prompt_toolkit.application import Application, run_in_terminal
|
|
22
|
+
from prompt_toolkit.application.current import get_app
|
|
23
|
+
from prompt_toolkit.cursor_shapes import CursorShape
|
|
24
|
+
from prompt_toolkit.data_structures import Point
|
|
25
|
+
from prompt_toolkit.filters import Condition
|
|
26
|
+
from prompt_toolkit.formatted_text import FormattedText, to_formatted_text
|
|
27
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
28
|
+
from prompt_toolkit.layout import Layout, WindowAlign
|
|
29
|
+
from prompt_toolkit.layout.containers import ConditionalContainer, HSplit, ScrollOffsets, Window
|
|
30
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
31
|
+
from prompt_toolkit.layout.margins import ScrollbarMargin
|
|
32
|
+
from prompt_toolkit.layout.processors import (
|
|
33
|
+
BeforeInput,
|
|
34
|
+
ConditionalProcessor,
|
|
35
|
+
PasswordProcessor,
|
|
36
|
+
)
|
|
37
|
+
from prompt_toolkit.styles import Style, merge_styles
|
|
38
|
+
from prompt_toolkit.widgets import Frame, TextArea
|
|
39
|
+
|
|
40
|
+
from .app import _ACCENT, _SPINNER_FRAMES, _render_picker_rows, _wrap_line
|
|
41
|
+
from .app import _STYLE as _CHAT_STYLE
|
|
42
|
+
from .owl import load_owl_animation
|
|
43
|
+
from .setup_flow import Row, SetupFlow
|
|
44
|
+
|
|
45
|
+
# Tones map to concrete styles. Green is the only accent — degraded/neutral
|
|
46
|
+
# states stay plain or amber so the accent reads clearly (same discipline as the
|
|
47
|
+
# chat TUI's panels).
|
|
48
|
+
_TONE_STYLE = {
|
|
49
|
+
"ok": "class:setup.ok",
|
|
50
|
+
"warn": "class:setup.warn",
|
|
51
|
+
"err": "class:setup.err",
|
|
52
|
+
"dim": "class:setup.dim",
|
|
53
|
+
"plain": "class:setup.text",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_SETUP_STYLE = Style.from_dict(
|
|
57
|
+
{
|
|
58
|
+
"setup.header": f"bold {_ACCENT}",
|
|
59
|
+
"setup.headerdim": "#6e7681",
|
|
60
|
+
"setup.title": f"bold {_ACCENT}",
|
|
61
|
+
"setup.subtitle": "#8b949e",
|
|
62
|
+
"setup.text": "#c9d1d9",
|
|
63
|
+
"setup.dim": "#6e7681",
|
|
64
|
+
# Prominent call-to-action (e.g. "Press Enter to begin") on message screens.
|
|
65
|
+
"setup.cta": f"bold {_ACCENT}",
|
|
66
|
+
"setup.ok": f"bold {_ACCENT}",
|
|
67
|
+
"setup.warn": "#d19a66",
|
|
68
|
+
"setup.err": "#e06c75",
|
|
69
|
+
"setup.spinner": f"bold {_ACCENT}",
|
|
70
|
+
"setup.footer.brand": f"bold {_ACCENT}",
|
|
71
|
+
"setup.footer.dim": "#6e7681",
|
|
72
|
+
"setup.footer.progress": "#8b949e",
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
_STYLE = merge_styles([_CHAT_STYLE, _SETUP_STYLE])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _row_to_dict(row: Row) -> dict[str, Any]:
|
|
80
|
+
return {
|
|
81
|
+
"label": row.label,
|
|
82
|
+
"description": row.description,
|
|
83
|
+
"value": row.value,
|
|
84
|
+
"current": row.current,
|
|
85
|
+
"kind": row.kind,
|
|
86
|
+
"tone": row.tone,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_setup_tui(
|
|
91
|
+
*,
|
|
92
|
+
owl_color: bool = True,
|
|
93
|
+
input: Any | None = None,
|
|
94
|
+
output: Any | None = None,
|
|
95
|
+
inline_busy: bool = False,
|
|
96
|
+
) -> bool:
|
|
97
|
+
"""Run the alt-screen setup wizard.
|
|
98
|
+
|
|
99
|
+
Returns ``True`` when setup was saved, ``False`` when the user cancelled.
|
|
100
|
+
Raises on infrastructure failure (no usable terminal, prompt_toolkit error)
|
|
101
|
+
so the caller can fall back to the classic Rich wizard.
|
|
102
|
+
|
|
103
|
+
``inline_busy`` runs the network/validation steps synchronously inside the
|
|
104
|
+
key handler instead of on a worker thread; tests use it so a pre-loaded key
|
|
105
|
+
sequence drives the whole flow deterministically.
|
|
106
|
+
"""
|
|
107
|
+
flow = SetupFlow()
|
|
108
|
+
owl = load_owl_animation(color_enabled=owl_color)
|
|
109
|
+
|
|
110
|
+
spinner = {"i": 0}
|
|
111
|
+
busy = {"running": False}
|
|
112
|
+
report = {"text": ""}
|
|
113
|
+
# Cached cursor row for the scrollable body so the selected list option stays
|
|
114
|
+
# in view (the chat TUI's cursor-pin trick, mirrored here).
|
|
115
|
+
body_cursor = {"row": 0}
|
|
116
|
+
# Tracks the last stage the input box was primed for, so we reset/seed the
|
|
117
|
+
# buffer exactly once per stage change (never mid-edit).
|
|
118
|
+
last_stage: dict[str, Any] = {"v": None}
|
|
119
|
+
|
|
120
|
+
def _invalidate() -> None:
|
|
121
|
+
try:
|
|
122
|
+
get_app().invalidate()
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
flow.set_report(lambda msg: (report.__setitem__("text", str(msg)), _invalidate()))
|
|
127
|
+
|
|
128
|
+
# ----------------------------------------------------------- rendering
|
|
129
|
+
def _width(win: Window, fallback: int = 80) -> int:
|
|
130
|
+
info = win.render_info
|
|
131
|
+
if info is not None and info.window_width:
|
|
132
|
+
return int(info.window_width)
|
|
133
|
+
try:
|
|
134
|
+
return max(20, get_app().output.get_size().columns - 2)
|
|
135
|
+
except Exception:
|
|
136
|
+
return fallback
|
|
137
|
+
|
|
138
|
+
def _header_text() -> FormattedText:
|
|
139
|
+
return FormattedText(
|
|
140
|
+
[
|
|
141
|
+
("class:setup.header", "◇ alysis"),
|
|
142
|
+
("class:setup.headerdim", " · setup"),
|
|
143
|
+
]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _message_fragments() -> FormattedText:
|
|
147
|
+
scr = flow.screen()
|
|
148
|
+
frags: list[tuple[str, str]] = [("", "\n")]
|
|
149
|
+
if scr.stage == "welcome":
|
|
150
|
+
owl_ansi = owl.current_ansi()
|
|
151
|
+
if owl_ansi is not None:
|
|
152
|
+
frags.extend(to_formatted_text(owl_ansi))
|
|
153
|
+
frags.append(("", "\n\n"))
|
|
154
|
+
if scr.title:
|
|
155
|
+
frags.append(("class:setup.title", scr.title))
|
|
156
|
+
frags.append(("", "\n"))
|
|
157
|
+
if scr.subtitle:
|
|
158
|
+
frags.append(("class:setup.subtitle", scr.subtitle))
|
|
159
|
+
frags.append(("", "\n"))
|
|
160
|
+
frags.append(("", "\n"))
|
|
161
|
+
for text, tone in scr.lines:
|
|
162
|
+
frags.append((_TONE_STYLE.get(tone, "class:setup.text"), text))
|
|
163
|
+
frags.append(("", "\n"))
|
|
164
|
+
if scr.hint:
|
|
165
|
+
# The call-to-action stands out in bold green with a blank line above.
|
|
166
|
+
frags.append(("", "\n\n"))
|
|
167
|
+
frags.append(("class:setup.cta", scr.hint))
|
|
168
|
+
frags.append(("", "\n"))
|
|
169
|
+
return FormattedText(frags)
|
|
170
|
+
|
|
171
|
+
def _body_fragments() -> FormattedText:
|
|
172
|
+
scr = flow.screen()
|
|
173
|
+
width = _width(body_window)
|
|
174
|
+
rows: list[list[tuple[str, str]]] = []
|
|
175
|
+
# Title + subtitle header. Both are pre-wrapped to the content width so
|
|
176
|
+
# header_lines equals the true screen-row count — the cursor-pin scroll
|
|
177
|
+
# math (which counts logical rows) requires every emitted row <= width.
|
|
178
|
+
if scr.title:
|
|
179
|
+
for line in _wrap_line(scr.title, width):
|
|
180
|
+
rows.append([("class:setup.title", line)])
|
|
181
|
+
if scr.subtitle:
|
|
182
|
+
for line in _wrap_line(scr.subtitle, width):
|
|
183
|
+
rows.append([("class:setup.subtitle", line)])
|
|
184
|
+
rows.append([("", "")]) # blank spacer
|
|
185
|
+
header_lines = len(rows)
|
|
186
|
+
sel_line = header_lines
|
|
187
|
+
|
|
188
|
+
if scr.mode == "list" and scr.rows:
|
|
189
|
+
picker = _render_picker_rows(
|
|
190
|
+
[_row_to_dict(r) for r in scr.rows], scr.index, width, hint=scr.hint
|
|
191
|
+
)
|
|
192
|
+
located = next(
|
|
193
|
+
(i for i, prow in enumerate(picker) if any("selcaret" in s for s, _ in prow)),
|
|
194
|
+
0,
|
|
195
|
+
)
|
|
196
|
+
sel_line = header_lines + located
|
|
197
|
+
rows.extend(picker)
|
|
198
|
+
elif scr.mode == "confirm":
|
|
199
|
+
for text, tone in scr.lines:
|
|
200
|
+
for line in _wrap_line(text, width):
|
|
201
|
+
rows.append([(_TONE_STYLE.get(tone, "class:setup.text"), line)])
|
|
202
|
+
rows.append([("", "")])
|
|
203
|
+
if scr.hint:
|
|
204
|
+
rows.append([("class:setup.dim", scr.hint)])
|
|
205
|
+
elif scr.mode == "input":
|
|
206
|
+
if scr.input_label:
|
|
207
|
+
rows.append([("class:setup.text", scr.input_label + ":")])
|
|
208
|
+
if scr.input_default:
|
|
209
|
+
rows.append(
|
|
210
|
+
[("class:setup.dim", f"default: {scr.input_default} (Enter to accept)")]
|
|
211
|
+
)
|
|
212
|
+
elif scr.mode == "busy":
|
|
213
|
+
frame = _SPINNER_FRAMES[spinner["i"] % len(_SPINNER_FRAMES)]
|
|
214
|
+
detail = report["text"] or scr.busy_label
|
|
215
|
+
rows.append([("class:setup.spinner", f"{frame} "), ("class:setup.text", detail)])
|
|
216
|
+
|
|
217
|
+
body_cursor["row"] = sel_line
|
|
218
|
+
out: list[tuple[str, str]] = []
|
|
219
|
+
for row in rows:
|
|
220
|
+
out.extend(row)
|
|
221
|
+
out.append(("", "\n"))
|
|
222
|
+
return FormattedText(out)
|
|
223
|
+
|
|
224
|
+
def _status_fragments() -> FormattedText:
|
|
225
|
+
scr = flow.screen()
|
|
226
|
+
if scr.mode == "busy":
|
|
227
|
+
return FormattedText([("", "")])
|
|
228
|
+
if not scr.status:
|
|
229
|
+
return FormattedText([("", "")])
|
|
230
|
+
return FormattedText([(_TONE_STYLE.get(scr.status_tone, "class:setup.dim"), scr.status)])
|
|
231
|
+
|
|
232
|
+
def _footer_fragments() -> FormattedText:
|
|
233
|
+
scr = flow.screen()
|
|
234
|
+
try:
|
|
235
|
+
width = get_app().output.get_size().columns
|
|
236
|
+
except Exception:
|
|
237
|
+
width = 80
|
|
238
|
+
left: list[tuple[str, str]] = [("class:setup.footer.progress", scr.progress)]
|
|
239
|
+
# The per-screen hint already lives in the body (picker/confirm/input);
|
|
240
|
+
# keep the footer to the progress label + a global cancel hint.
|
|
241
|
+
right: list[tuple[str, str]] = [("class:setup.footer.dim", "Ctrl+C cancel")]
|
|
242
|
+
left_len = sum(len(t) for _s, t in left)
|
|
243
|
+
right_len = sum(len(t) for _s, t in right)
|
|
244
|
+
gap = max(1, width - left_len - right_len)
|
|
245
|
+
return FormattedText([*left, ("", " " * gap), *right])
|
|
246
|
+
|
|
247
|
+
# ----------------------------------------------------------- windows
|
|
248
|
+
# message_window and body_window are focusable so focus can rest on the window
|
|
249
|
+
# that is actually visible for the current screen (no stray cursor parked on a
|
|
250
|
+
# hidden input box). They render no editable cursor (message) / a cursor on the
|
|
251
|
+
# selected row (body), and ignore typed keys — all input is global bindings.
|
|
252
|
+
message_window = Window(
|
|
253
|
+
FormattedTextControl(_message_fragments, focusable=True),
|
|
254
|
+
align=WindowAlign.CENTER,
|
|
255
|
+
)
|
|
256
|
+
body_window = Window(
|
|
257
|
+
FormattedTextControl(
|
|
258
|
+
_body_fragments,
|
|
259
|
+
focusable=True,
|
|
260
|
+
get_cursor_position=lambda: Point(0, body_cursor["row"]),
|
|
261
|
+
),
|
|
262
|
+
wrap_lines=True,
|
|
263
|
+
scroll_offsets=ScrollOffsets(top=1, bottom=1),
|
|
264
|
+
right_margins=[ScrollbarMargin(display_arrows=False)],
|
|
265
|
+
)
|
|
266
|
+
status_window = Window(FormattedTextControl(_status_fragments, focusable=False), height=1)
|
|
267
|
+
|
|
268
|
+
_is_password = Condition(lambda: flow.screen().input_password)
|
|
269
|
+
input_area = TextArea(
|
|
270
|
+
height=1,
|
|
271
|
+
multiline=False,
|
|
272
|
+
wrap_lines=False,
|
|
273
|
+
style="class:tui.input",
|
|
274
|
+
input_processors=[
|
|
275
|
+
ConditionalProcessor(PasswordProcessor(), filter=_is_password),
|
|
276
|
+
BeforeInput("> ", style="class:tui.prompt"),
|
|
277
|
+
],
|
|
278
|
+
)
|
|
279
|
+
input_frame = Frame(input_area)
|
|
280
|
+
|
|
281
|
+
is_message = Condition(lambda: flow.current_mode() == "message")
|
|
282
|
+
is_panel = Condition(lambda: flow.current_mode() in {"list", "input", "confirm", "busy"})
|
|
283
|
+
is_input = Condition(lambda: flow.current_mode() == "input")
|
|
284
|
+
is_busy = Condition(lambda: flow.current_mode() == "busy")
|
|
285
|
+
is_list = Condition(lambda: flow.current_mode() == "list")
|
|
286
|
+
is_confirm = Condition(lambda: flow.current_mode() == "confirm")
|
|
287
|
+
has_status = Condition(lambda: flow.current_mode() != "busy" and bool(flow.screen().status))
|
|
288
|
+
|
|
289
|
+
# The input box (and its focus) is the ONLY focusable window, so focus rests on
|
|
290
|
+
# it the whole time — but it is only *shown* on input steps (a hidden
|
|
291
|
+
# ConditionalContainer keeps the window in the layout tree, so focus stays
|
|
292
|
+
# valid and the worker can flip to an input step with no cross-thread focus
|
|
293
|
+
# change). Welcome / picker / confirm screens therefore have no stray text box.
|
|
294
|
+
root = HSplit(
|
|
295
|
+
[
|
|
296
|
+
Window(FormattedTextControl(_header_text, focusable=False), height=1),
|
|
297
|
+
Window(height=1),
|
|
298
|
+
ConditionalContainer(message_window, filter=is_message),
|
|
299
|
+
ConditionalContainer(body_window, filter=is_panel),
|
|
300
|
+
ConditionalContainer(status_window, filter=has_status | is_busy),
|
|
301
|
+
ConditionalContainer(input_frame, filter=is_input),
|
|
302
|
+
Window(height=1),
|
|
303
|
+
Window(FormattedTextControl(_footer_fragments, focusable=False), height=1),
|
|
304
|
+
]
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# ----------------------------------------------------------- focus / pump
|
|
308
|
+
def _reconcile_focus() -> None:
|
|
309
|
+
# Keep focus on the window that is actually visible for the current
|
|
310
|
+
# screen, so the cursor never parks on a hidden input box.
|
|
311
|
+
mode = flow.current_mode()
|
|
312
|
+
if mode == "input":
|
|
313
|
+
target_window = input_area.window
|
|
314
|
+
elif mode == "message":
|
|
315
|
+
target_window = message_window
|
|
316
|
+
else:
|
|
317
|
+
target_window = body_window
|
|
318
|
+
try:
|
|
319
|
+
app = get_app()
|
|
320
|
+
if app.layout.current_window is not target_window:
|
|
321
|
+
app.layout.focus(target_window)
|
|
322
|
+
except Exception:
|
|
323
|
+
pass
|
|
324
|
+
|
|
325
|
+
def _schedule_focus() -> None:
|
|
326
|
+
# Move focus from a worker thread safely (only app.invalidate() and
|
|
327
|
+
# loop.call_soon_threadsafe are thread-safe in prompt_toolkit).
|
|
328
|
+
try:
|
|
329
|
+
loop = getattr(get_app(), "loop", None)
|
|
330
|
+
if loop is not None:
|
|
331
|
+
loop.call_soon_threadsafe(_reconcile_focus)
|
|
332
|
+
except Exception:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
def _busy_worker() -> None:
|
|
336
|
+
while True:
|
|
337
|
+
if flow.current_mode() != "busy" or flow.busy_kind() != "thread":
|
|
338
|
+
break
|
|
339
|
+
try:
|
|
340
|
+
flow.run_busy()
|
|
341
|
+
except BaseException as exc: # noqa: BLE001 - surface any failure as fatal
|
|
342
|
+
flow.fatal_error = f"{type(exc).__name__}: {exc}"
|
|
343
|
+
flow.stage = "fatal"
|
|
344
|
+
report["text"] = ""
|
|
345
|
+
_invalidate()
|
|
346
|
+
busy["running"] = False
|
|
347
|
+
_schedule_focus()
|
|
348
|
+
_invalidate()
|
|
349
|
+
|
|
350
|
+
async def _terminal_busy() -> None:
|
|
351
|
+
try:
|
|
352
|
+
await run_in_terminal(flow.run_busy, in_executor=True)
|
|
353
|
+
except BaseException as exc: # noqa: BLE001
|
|
354
|
+
flow.fatal_error = f"{type(exc).__name__}: {exc}"
|
|
355
|
+
flow.stage = "fatal"
|
|
356
|
+
report["text"] = ""
|
|
357
|
+
busy["running"] = False
|
|
358
|
+
_invalidate()
|
|
359
|
+
_pump()
|
|
360
|
+
|
|
361
|
+
def _pump() -> None:
|
|
362
|
+
# Exit once the flow terminates.
|
|
363
|
+
if flow.stage == "done":
|
|
364
|
+
try:
|
|
365
|
+
get_app().exit(result=bool(flow.success))
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
return
|
|
369
|
+
mode = flow.current_mode()
|
|
370
|
+
# Clear the input box once per stage change (UI thread only) so a prior
|
|
371
|
+
# step's text never lingers. We never *seed* the buffer — defaults are
|
|
372
|
+
# shown as a hint and the flow treats empty input as the default — so a
|
|
373
|
+
# worker flipping to an input step (key-retry / workspace) needs no
|
|
374
|
+
# cross-thread buffer write: the box was already cleared when its busy
|
|
375
|
+
# step began.
|
|
376
|
+
if flow.stage != last_stage["v"]:
|
|
377
|
+
if mode != "input" or not input_area.text:
|
|
378
|
+
input_area.text = ""
|
|
379
|
+
last_stage["v"] = flow.stage
|
|
380
|
+
_reconcile_focus()
|
|
381
|
+
# Dispatch a busy step to the right executor.
|
|
382
|
+
if mode == "busy" and not busy["running"]:
|
|
383
|
+
busy["running"] = True
|
|
384
|
+
report["text"] = ""
|
|
385
|
+
if inline_busy:
|
|
386
|
+
while flow.current_mode() == "busy":
|
|
387
|
+
try:
|
|
388
|
+
flow.run_busy()
|
|
389
|
+
except BaseException as exc: # noqa: BLE001
|
|
390
|
+
flow.fatal_error = f"{type(exc).__name__}: {exc}"
|
|
391
|
+
flow.stage = "fatal"
|
|
392
|
+
report["text"] = ""
|
|
393
|
+
busy["running"] = False
|
|
394
|
+
_pump() # handle whatever stage the chain landed on
|
|
395
|
+
return
|
|
396
|
+
if flow.busy_kind() == "thread":
|
|
397
|
+
threading.Thread(target=_busy_worker, daemon=True).start()
|
|
398
|
+
else:
|
|
399
|
+
try:
|
|
400
|
+
get_app().create_background_task(_terminal_busy())
|
|
401
|
+
except Exception:
|
|
402
|
+
# No running loop — run inline as a fallback, then re-pump so
|
|
403
|
+
# the stage it lands on (input prep / exit / a chained busy
|
|
404
|
+
# step) is handled, exactly like the other two busy paths.
|
|
405
|
+
try:
|
|
406
|
+
flow.run_busy()
|
|
407
|
+
except BaseException as exc: # noqa: BLE001
|
|
408
|
+
flow.fatal_error = f"{type(exc).__name__}: {exc}"
|
|
409
|
+
flow.stage = "fatal"
|
|
410
|
+
busy["running"] = False
|
|
411
|
+
_pump()
|
|
412
|
+
return
|
|
413
|
+
_invalidate()
|
|
414
|
+
|
|
415
|
+
# ----------------------------------------------------------- key bindings
|
|
416
|
+
kb = KeyBindings()
|
|
417
|
+
|
|
418
|
+
@kb.add("c-c")
|
|
419
|
+
@kb.add("c-d")
|
|
420
|
+
def _cancel(event: Any) -> None:
|
|
421
|
+
# Always abort setup immediately so the user is never trapped — including
|
|
422
|
+
# mid-validation (the daemon worker, if any, dies with the process) and on
|
|
423
|
+
# the cancel-confirm screen. Returns ``False`` (cancelled) to the caller.
|
|
424
|
+
flow.success = False
|
|
425
|
+
try:
|
|
426
|
+
event.app.exit(result=False)
|
|
427
|
+
except Exception:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
@kb.add("enter", eager=True, filter=~is_busy)
|
|
431
|
+
def _enter(event: Any) -> None:
|
|
432
|
+
mode = flow.current_mode()
|
|
433
|
+
if mode == "input":
|
|
434
|
+
flow.submit_input(input_area.text)
|
|
435
|
+
elif mode == "list":
|
|
436
|
+
flow.choose_current()
|
|
437
|
+
elif mode == "message":
|
|
438
|
+
flow.advance_message()
|
|
439
|
+
elif mode == "confirm":
|
|
440
|
+
flow.confirm(flow.screen().confirm_default)
|
|
441
|
+
_pump()
|
|
442
|
+
|
|
443
|
+
@kb.add("escape", filter=~is_busy)
|
|
444
|
+
def _escape(event: Any) -> None:
|
|
445
|
+
flow.back()
|
|
446
|
+
_pump()
|
|
447
|
+
|
|
448
|
+
@kb.add("up", filter=is_list, eager=True)
|
|
449
|
+
@kb.add("k", filter=is_list, eager=True)
|
|
450
|
+
def _up(event: Any) -> None:
|
|
451
|
+
flow.move(-1)
|
|
452
|
+
_invalidate()
|
|
453
|
+
|
|
454
|
+
@kb.add("down", filter=is_list, eager=True)
|
|
455
|
+
@kb.add("j", filter=is_list, eager=True)
|
|
456
|
+
def _down(event: Any) -> None:
|
|
457
|
+
flow.move(1)
|
|
458
|
+
_invalidate()
|
|
459
|
+
|
|
460
|
+
for _digit in "123456789":
|
|
461
|
+
|
|
462
|
+
@kb.add(_digit, filter=is_list, eager=True)
|
|
463
|
+
def _pick(event: Any) -> None:
|
|
464
|
+
try:
|
|
465
|
+
flow.choose_index(int(event.data) - 1)
|
|
466
|
+
except (TypeError, ValueError):
|
|
467
|
+
return
|
|
468
|
+
_pump()
|
|
469
|
+
|
|
470
|
+
@kb.add("y", filter=is_confirm, eager=True)
|
|
471
|
+
@kb.add("Y", filter=is_confirm, eager=True)
|
|
472
|
+
def _yes(event: Any) -> None:
|
|
473
|
+
flow.confirm(True)
|
|
474
|
+
_pump()
|
|
475
|
+
|
|
476
|
+
@kb.add("n", filter=is_confirm, eager=True)
|
|
477
|
+
@kb.add("N", filter=is_confirm, eager=True)
|
|
478
|
+
def _no(event: Any) -> None:
|
|
479
|
+
flow.confirm(False)
|
|
480
|
+
_pump()
|
|
481
|
+
|
|
482
|
+
app: Application = Application(
|
|
483
|
+
layout=Layout(root, focused_element=message_window),
|
|
484
|
+
key_bindings=kb,
|
|
485
|
+
style=_STYLE,
|
|
486
|
+
full_screen=True,
|
|
487
|
+
mouse_support=False,
|
|
488
|
+
cursor=CursorShape.BEAM,
|
|
489
|
+
input=input,
|
|
490
|
+
output=output,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
def _pre_run() -> None:
|
|
494
|
+
# Prime the very first screen (start any immediate busy step / input box).
|
|
495
|
+
_pump()
|
|
496
|
+
|
|
497
|
+
def _spin() -> None:
|
|
498
|
+
while getattr(app, "is_running", False):
|
|
499
|
+
time.sleep(0.1)
|
|
500
|
+
animated = False
|
|
501
|
+
if owl.available and flow.current_mode() == "message" and flow.stage == "welcome":
|
|
502
|
+
owl.advance()
|
|
503
|
+
animated = True
|
|
504
|
+
if flow.current_mode() == "busy":
|
|
505
|
+
spinner["i"] = (spinner["i"] + 1) % len(_SPINNER_FRAMES)
|
|
506
|
+
animated = True
|
|
507
|
+
if animated:
|
|
508
|
+
try:
|
|
509
|
+
app.invalidate()
|
|
510
|
+
except Exception:
|
|
511
|
+
break
|
|
512
|
+
|
|
513
|
+
threading.Thread(target=_spin, daemon=True).start()
|
|
514
|
+
|
|
515
|
+
result = app.run(pre_run=_pre_run)
|
|
516
|
+
return bool(result)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
__all__ = ["run_setup_tui"]
|