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,566 @@
|
|
|
1
|
+
"""In-app overlay that hosts the TUI configuration menu (:class:`ConfigFlow`).
|
|
2
|
+
|
|
3
|
+
Unlike the setup wizard (:mod:`cli_impl.tui.setup_app`), which is a standalone
|
|
4
|
+
:class:`~prompt_toolkit.application.Application` run before chat, ``/config`` must
|
|
5
|
+
open *while the chat application is already running* — the transcript, session, and
|
|
6
|
+
scroll position all need to survive. So this is not a second ``Application`` but a
|
|
7
|
+
full-screen :class:`~prompt_toolkit.layout.containers.Float` added to the chat app's
|
|
8
|
+
``FloatContainer``, plus key bindings registered on the chat app's
|
|
9
|
+
``KeyBindings``. It reuses the chat/setup render grammar (the picker renderer, the
|
|
10
|
+
``setup.*`` style classes, the cursor-pin scrollable body) so it looks identical to
|
|
11
|
+
the rest of the TUI.
|
|
12
|
+
|
|
13
|
+
The host (:mod:`cli_impl.tui.app`) wires it up:
|
|
14
|
+
|
|
15
|
+
* appends :attr:`ConfigOverlay.float` to the ``FloatContainer`` floats,
|
|
16
|
+
* calls :meth:`ConfigOverlay.register` to add the overlay's key bindings,
|
|
17
|
+
* gates its own global bindings with ``& ~overlay.open_condition`` so they don't
|
|
18
|
+
fire underneath the overlay,
|
|
19
|
+
* routes bare ``/config`` to :meth:`ConfigOverlay.open` and Ctrl+C to
|
|
20
|
+
:meth:`ConfigOverlay.request_cancel` while open.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import threading
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from prompt_toolkit.application.current import get_app
|
|
30
|
+
from prompt_toolkit.data_structures import Point
|
|
31
|
+
from prompt_toolkit.filters import Condition
|
|
32
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
33
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
34
|
+
from prompt_toolkit.layout.containers import (
|
|
35
|
+
ConditionalContainer,
|
|
36
|
+
Float,
|
|
37
|
+
HSplit,
|
|
38
|
+
ScrollOffsets,
|
|
39
|
+
Window,
|
|
40
|
+
)
|
|
41
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
42
|
+
from prompt_toolkit.layout.margins import ScrollbarMargin
|
|
43
|
+
from prompt_toolkit.layout.processors import (
|
|
44
|
+
BeforeInput,
|
|
45
|
+
ConditionalProcessor,
|
|
46
|
+
PasswordProcessor,
|
|
47
|
+
)
|
|
48
|
+
from prompt_toolkit.widgets import Frame, TextArea
|
|
49
|
+
|
|
50
|
+
from .app import (
|
|
51
|
+
_SPINNER_FRAMES,
|
|
52
|
+
_render_picker_rows,
|
|
53
|
+
_transcript_content_width_for,
|
|
54
|
+
_wrap_line,
|
|
55
|
+
)
|
|
56
|
+
from .setup_app import _TONE_STYLE, _row_to_dict
|
|
57
|
+
|
|
58
|
+
# Opaque dark background so the full-screen float fully covers the chat behind it.
|
|
59
|
+
_BG = "bg:#0d1117"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ConfigOverlay:
|
|
63
|
+
"""A full-screen ``/config`` overlay driven by a :class:`ConfigFlow`."""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
flow_factory: Callable[[], Any],
|
|
69
|
+
on_saved: Callable[[int], None] | None = None,
|
|
70
|
+
on_error: Callable[[str], None] | None = None,
|
|
71
|
+
on_switch_workspace: Callable[[str], None] | None = None,
|
|
72
|
+
focus_chat: Callable[[], None] | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
self._flow_factory = flow_factory
|
|
75
|
+
self._on_saved = on_saved
|
|
76
|
+
self._on_error = on_error
|
|
77
|
+
self._on_switch = on_switch_workspace
|
|
78
|
+
self._focus_chat = focus_chat
|
|
79
|
+
self.flow: Any | None = None
|
|
80
|
+
self._open = {"on": False}
|
|
81
|
+
self._busy = {"running": False}
|
|
82
|
+
self._spinner = {"i": 0}
|
|
83
|
+
self._body_cursor = {"row": 0}
|
|
84
|
+
self._last_field: dict[str, Any] = {"key": None}
|
|
85
|
+
|
|
86
|
+
self.open_condition = Condition(lambda: self._open["on"])
|
|
87
|
+
|
|
88
|
+
# ----------------------------------------------------------- windows
|
|
89
|
+
self._body_window = Window(
|
|
90
|
+
FormattedTextControl(
|
|
91
|
+
self._body_fragments,
|
|
92
|
+
focusable=True,
|
|
93
|
+
get_cursor_position=lambda: Point(0, self._body_cursor["row"]),
|
|
94
|
+
),
|
|
95
|
+
wrap_lines=True,
|
|
96
|
+
scroll_offsets=ScrollOffsets(top=1, bottom=1),
|
|
97
|
+
right_margins=[ScrollbarMargin(display_arrows=False)],
|
|
98
|
+
style=_BG,
|
|
99
|
+
)
|
|
100
|
+
self._status_window = Window(
|
|
101
|
+
FormattedTextControl(self._status_fragments, focusable=False),
|
|
102
|
+
height=1,
|
|
103
|
+
style=_BG,
|
|
104
|
+
)
|
|
105
|
+
# Mask the input whenever the active input screen asks for it.
|
|
106
|
+
_is_password = Condition(
|
|
107
|
+
lambda: (
|
|
108
|
+
self._open["on"]
|
|
109
|
+
and self.flow is not None
|
|
110
|
+
and self.flow.current_mode() == "input"
|
|
111
|
+
and bool(getattr(self.flow.screen(), "input_password", False))
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
self._input = TextArea(
|
|
115
|
+
height=1,
|
|
116
|
+
multiline=False,
|
|
117
|
+
wrap_lines=False,
|
|
118
|
+
style="class:tui.input",
|
|
119
|
+
input_processors=[
|
|
120
|
+
ConditionalProcessor(PasswordProcessor(), filter=_is_password),
|
|
121
|
+
BeforeInput("> ", style="class:tui.prompt"),
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
self._input_frame = Frame(self._input)
|
|
125
|
+
|
|
126
|
+
_is_input = Condition(lambda: self._open["on"] and self._mode() == "input")
|
|
127
|
+
|
|
128
|
+
body = HSplit(
|
|
129
|
+
[
|
|
130
|
+
Window(
|
|
131
|
+
FormattedTextControl(self._header_fragments, focusable=False),
|
|
132
|
+
height=1,
|
|
133
|
+
style=_BG,
|
|
134
|
+
),
|
|
135
|
+
Window(height=1, style=_BG),
|
|
136
|
+
self._body_window,
|
|
137
|
+
self._status_window,
|
|
138
|
+
ConditionalContainer(self._input_frame, filter=_is_input),
|
|
139
|
+
Window(height=1, style=_BG),
|
|
140
|
+
Window(
|
|
141
|
+
FormattedTextControl(self._footer_fragments, focusable=False),
|
|
142
|
+
height=1,
|
|
143
|
+
style=_BG,
|
|
144
|
+
),
|
|
145
|
+
],
|
|
146
|
+
style=_BG,
|
|
147
|
+
)
|
|
148
|
+
self.float = Float(
|
|
149
|
+
content=ConditionalContainer(body, filter=self.open_condition),
|
|
150
|
+
left=0,
|
|
151
|
+
right=0,
|
|
152
|
+
top=0,
|
|
153
|
+
bottom=0,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------- helpers
|
|
157
|
+
|
|
158
|
+
def is_open(self) -> bool:
|
|
159
|
+
return self._open["on"]
|
|
160
|
+
|
|
161
|
+
def is_busy(self) -> bool:
|
|
162
|
+
return self._open["on"] and self.flow is not None and self.flow.current_mode() == "busy"
|
|
163
|
+
|
|
164
|
+
def tick_spinner(self) -> None:
|
|
165
|
+
"""Advance the saving spinner one frame (driven by the app's repaint loop)."""
|
|
166
|
+
self._spinner["i"] = (self._spinner["i"] + 1) % len(_SPINNER_FRAMES)
|
|
167
|
+
|
|
168
|
+
def _mode(self) -> str:
|
|
169
|
+
return self.flow.current_mode() if self.flow is not None else "message"
|
|
170
|
+
|
|
171
|
+
def _invalidate(self) -> None:
|
|
172
|
+
try:
|
|
173
|
+
get_app().invalidate()
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def _width(self) -> int:
|
|
178
|
+
# Derived from the terminal, not read back from render_info: the overlay
|
|
179
|
+
# body is a full-screen float (no frame) carrying one scrollbar margin, so
|
|
180
|
+
# it is exactly one column narrower than the screen. render_info is a frame
|
|
181
|
+
# BEHIND — after a resize it still reports the old, wider size, and the
|
|
182
|
+
# rows built from it overflow and hard-wrap into the new, narrower window.
|
|
183
|
+
try:
|
|
184
|
+
cols = get_app().output.get_size().columns
|
|
185
|
+
except Exception:
|
|
186
|
+
return 80
|
|
187
|
+
return max(20, _transcript_content_width_for(cols))
|
|
188
|
+
|
|
189
|
+
# ------------------------------------------------------------- rendering
|
|
190
|
+
|
|
191
|
+
def _header_fragments(self) -> FormattedText:
|
|
192
|
+
return FormattedText(
|
|
193
|
+
[("class:setup.header", "◇ alysis"), ("class:setup.headerdim", " · config")]
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def _body_fragments(self) -> FormattedText:
|
|
197
|
+
flow = self.flow
|
|
198
|
+
if flow is None:
|
|
199
|
+
return FormattedText([("", "")])
|
|
200
|
+
scr = flow.screen()
|
|
201
|
+
width = self._width()
|
|
202
|
+
rows: list[list[tuple[str, str]]] = []
|
|
203
|
+
if scr.title:
|
|
204
|
+
for line in _wrap_line(scr.title, width):
|
|
205
|
+
rows.append([("class:setup.title", line)])
|
|
206
|
+
if scr.subtitle:
|
|
207
|
+
for line in _wrap_line(scr.subtitle, width):
|
|
208
|
+
rows.append([("class:setup.subtitle", line)])
|
|
209
|
+
rows.append([("", "")])
|
|
210
|
+
header_lines = len(rows)
|
|
211
|
+
sel_line = header_lines
|
|
212
|
+
|
|
213
|
+
if scr.mode == "list" and scr.rows:
|
|
214
|
+
picker = _render_picker_rows(
|
|
215
|
+
[_row_to_dict(r) for r in scr.rows],
|
|
216
|
+
scr.index,
|
|
217
|
+
width,
|
|
218
|
+
hint=scr.hint,
|
|
219
|
+
numbered=getattr(scr, "numbered", True),
|
|
220
|
+
)
|
|
221
|
+
located = next(
|
|
222
|
+
(i for i, prow in enumerate(picker) if any("selcaret" in s for s, _ in prow)),
|
|
223
|
+
0,
|
|
224
|
+
)
|
|
225
|
+
sel_line = header_lines + located
|
|
226
|
+
rows.extend(picker)
|
|
227
|
+
elif scr.mode == "confirm":
|
|
228
|
+
for text, tone in scr.lines:
|
|
229
|
+
for line in _wrap_line(text, width):
|
|
230
|
+
rows.append([(_TONE_STYLE.get(tone, "class:setup.text"), line)])
|
|
231
|
+
rows.append([("", "")])
|
|
232
|
+
if scr.hint:
|
|
233
|
+
rows.append([("class:setup.dim", scr.hint)])
|
|
234
|
+
elif scr.mode == "input":
|
|
235
|
+
if scr.input_label:
|
|
236
|
+
for line in _wrap_line(scr.input_label + ":", width):
|
|
237
|
+
rows.append([("class:setup.text", line)])
|
|
238
|
+
if scr.input_default and not scr.input_password:
|
|
239
|
+
for line in _wrap_line(f"current: {scr.input_default}", width):
|
|
240
|
+
rows.append([("class:setup.dim", line)])
|
|
241
|
+
# No in-body hint: the footer is the single key legend and shows
|
|
242
|
+
# this screen's action verb (see _footer_fragments).
|
|
243
|
+
elif scr.mode == "busy":
|
|
244
|
+
frame = _SPINNER_FRAMES[self._spinner["i"] % len(_SPINNER_FRAMES)]
|
|
245
|
+
rows.append(
|
|
246
|
+
[
|
|
247
|
+
("class:setup.spinner", f"{frame} "),
|
|
248
|
+
("class:setup.text", scr.busy_label or "Working…"),
|
|
249
|
+
]
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
self._body_cursor["row"] = sel_line
|
|
253
|
+
out: list[tuple[str, str]] = []
|
|
254
|
+
for row in rows:
|
|
255
|
+
out.extend(row)
|
|
256
|
+
out.append(("", "\n"))
|
|
257
|
+
return FormattedText(out)
|
|
258
|
+
|
|
259
|
+
def _status_fragments(self) -> FormattedText:
|
|
260
|
+
flow = self.flow
|
|
261
|
+
if flow is None:
|
|
262
|
+
return FormattedText([("", "")])
|
|
263
|
+
scr = flow.screen()
|
|
264
|
+
if scr.mode == "busy" or not scr.status:
|
|
265
|
+
return FormattedText([("", "")])
|
|
266
|
+
return FormattedText([(_TONE_STYLE.get(scr.status_tone, "class:setup.dim"), scr.status)])
|
|
267
|
+
|
|
268
|
+
def _footer_fragments(self) -> FormattedText:
|
|
269
|
+
flow = self.flow
|
|
270
|
+
try:
|
|
271
|
+
width = get_app().output.get_size().columns
|
|
272
|
+
except Exception:
|
|
273
|
+
width = 80
|
|
274
|
+
crumb = flow._breadcrumb() if flow is not None else "configuration"
|
|
275
|
+
left = [
|
|
276
|
+
("class:setup.footer.brand", "◇ alysis"),
|
|
277
|
+
("class:setup.footer.dim", f" · {crumb}"),
|
|
278
|
+
]
|
|
279
|
+
is_menu = flow is not None and getattr(flow, "stage", "") == "menu"
|
|
280
|
+
mode = self._mode()
|
|
281
|
+
if mode == "list":
|
|
282
|
+
if is_menu:
|
|
283
|
+
right_text = "↑↓ move · Enter open · s save · Esc close"
|
|
284
|
+
else:
|
|
285
|
+
# Advertise digit shortcuts only on numbered lists; the footer is
|
|
286
|
+
# the single key legend now that in-body nav hints are gone.
|
|
287
|
+
numbered = True
|
|
288
|
+
if flow is not None:
|
|
289
|
+
try:
|
|
290
|
+
numbered = bool(getattr(flow.screen(), "numbered", True))
|
|
291
|
+
except Exception:
|
|
292
|
+
numbered = True
|
|
293
|
+
right_text = (
|
|
294
|
+
"↑↓ move · 1-9 pick · Enter select · Esc back"
|
|
295
|
+
if numbered
|
|
296
|
+
else "↑↓ move · Enter select · Esc back"
|
|
297
|
+
)
|
|
298
|
+
elif mode == "input":
|
|
299
|
+
# Honour the screen's own action verb ("Enter save", "Enter add",
|
|
300
|
+
# …) so the legend never contradicts what Enter actually does.
|
|
301
|
+
screen_hint = ""
|
|
302
|
+
if flow is not None:
|
|
303
|
+
try:
|
|
304
|
+
screen_hint = str(getattr(flow.screen(), "hint", "") or "")
|
|
305
|
+
except Exception:
|
|
306
|
+
screen_hint = ""
|
|
307
|
+
right_text = (
|
|
308
|
+
f"{screen_hint} · Ctrl+C cancel"
|
|
309
|
+
if screen_hint
|
|
310
|
+
else "Enter next · Esc back · Ctrl+C cancel"
|
|
311
|
+
)
|
|
312
|
+
elif mode == "confirm":
|
|
313
|
+
right_text = "Y / N · Esc back"
|
|
314
|
+
elif mode == "busy":
|
|
315
|
+
right_text = "Working…"
|
|
316
|
+
else:
|
|
317
|
+
right_text = "Ctrl+C cancel"
|
|
318
|
+
right = [("class:setup.footer.dim", right_text)]
|
|
319
|
+
left_len = sum(len(t) for _s, t in left)
|
|
320
|
+
right_len = sum(len(t) for _s, t in right)
|
|
321
|
+
gap = max(1, width - left_len - right_len)
|
|
322
|
+
return FormattedText([*left, ("", " " * gap), *right])
|
|
323
|
+
|
|
324
|
+
# ------------------------------------------------------------- lifecycle
|
|
325
|
+
|
|
326
|
+
def open(self, flow_factory: Callable[[], Any] | None = None) -> None:
|
|
327
|
+
if self._open["on"]:
|
|
328
|
+
return
|
|
329
|
+
try:
|
|
330
|
+
self.flow = (flow_factory or self._flow_factory)()
|
|
331
|
+
except Exception as exc: # noqa: BLE001 - surface, don't crash the chat UI
|
|
332
|
+
if self._on_error is not None:
|
|
333
|
+
try:
|
|
334
|
+
self._on_error(f"configuration could not open: {exc}")
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
self.flow = None
|
|
338
|
+
return
|
|
339
|
+
self._open["on"] = True
|
|
340
|
+
self._busy["running"] = False
|
|
341
|
+
self._spinner["i"] = 0
|
|
342
|
+
self._last_field["key"] = None
|
|
343
|
+
self._pump()
|
|
344
|
+
|
|
345
|
+
def request_cancel(self) -> None:
|
|
346
|
+
if not self._open["on"] or self.flow is None:
|
|
347
|
+
return
|
|
348
|
+
if self.flow.current_mode() == "busy":
|
|
349
|
+
return
|
|
350
|
+
self.flow.request_cancel()
|
|
351
|
+
self._pump()
|
|
352
|
+
|
|
353
|
+
def _close(self, *, saved: bool) -> None:
|
|
354
|
+
on_saved = self._on_saved
|
|
355
|
+
on_switch = self._on_switch
|
|
356
|
+
flow = self.flow
|
|
357
|
+
count = int(getattr(flow, "changes_count", 0) or 0) if flow is not None else 0
|
|
358
|
+
switch_target = getattr(flow, "switch_workspace", None) if flow is not None else None
|
|
359
|
+
self._open["on"] = False
|
|
360
|
+
self.flow = None
|
|
361
|
+
self._busy["running"] = False
|
|
362
|
+
self._last_field["key"] = None
|
|
363
|
+
try:
|
|
364
|
+
self._input.text = ""
|
|
365
|
+
except Exception:
|
|
366
|
+
pass
|
|
367
|
+
self._restore_focus()
|
|
368
|
+
self._invalidate()
|
|
369
|
+
# "Switch now" takes priority: hand the chosen folder to the host to relaunch
|
|
370
|
+
# the chat there (no "saved N changes" note — we're leaving this session).
|
|
371
|
+
if switch_target and on_switch is not None:
|
|
372
|
+
try:
|
|
373
|
+
on_switch(str(switch_target))
|
|
374
|
+
except Exception:
|
|
375
|
+
pass
|
|
376
|
+
return
|
|
377
|
+
if saved and on_saved is not None:
|
|
378
|
+
try:
|
|
379
|
+
on_saved(count)
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
# -------------------------------------------------------------- focus
|
|
384
|
+
|
|
385
|
+
def _reconcile_focus(self) -> None:
|
|
386
|
+
if not self._open["on"]:
|
|
387
|
+
return
|
|
388
|
+
mode = self._mode()
|
|
389
|
+
target = self._input.window if mode == "input" else self._body_window
|
|
390
|
+
try:
|
|
391
|
+
app = get_app()
|
|
392
|
+
if app.layout.current_window is not target:
|
|
393
|
+
app.layout.focus(target)
|
|
394
|
+
except Exception:
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
def _restore_focus(self) -> None:
|
|
398
|
+
if self._focus_chat is not None:
|
|
399
|
+
try:
|
|
400
|
+
self._focus_chat()
|
|
401
|
+
except Exception:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
# --------------------------------------------------------------- pump
|
|
405
|
+
|
|
406
|
+
def _pump(self) -> None:
|
|
407
|
+
flow = self.flow
|
|
408
|
+
if flow is None:
|
|
409
|
+
return
|
|
410
|
+
if flow.stage == "done":
|
|
411
|
+
self._close(saved=bool(flow.saved))
|
|
412
|
+
return
|
|
413
|
+
mode = flow.current_mode()
|
|
414
|
+
key = flow.field_key()
|
|
415
|
+
if key != self._last_field["key"]:
|
|
416
|
+
# Seed the input box on field change (UI thread only — the save worker
|
|
417
|
+
# never lands on an input stage, so this is race-free).
|
|
418
|
+
if mode == "input":
|
|
419
|
+
scr = flow.screen()
|
|
420
|
+
self._input.text = "" if scr.input_password else (scr.input_default or "")
|
|
421
|
+
self._input.buffer.cursor_position = len(self._input.text)
|
|
422
|
+
else:
|
|
423
|
+
self._input.text = ""
|
|
424
|
+
self._last_field["key"] = key
|
|
425
|
+
self._reconcile_focus()
|
|
426
|
+
if mode == "busy" and not self._busy["running"]:
|
|
427
|
+
self._busy["running"] = True
|
|
428
|
+
threading.Thread(target=self._busy_worker, daemon=True).start()
|
|
429
|
+
self._invalidate()
|
|
430
|
+
|
|
431
|
+
def _busy_worker(self) -> None:
|
|
432
|
+
# Run ONLY the blocking save I/O here; it records its outcome on the flow but
|
|
433
|
+
# writes no renderer-visible stage/status. The stage transition + close/reload
|
|
434
|
+
# are then applied on the UI thread (``_finalize_save``), so the render
|
|
435
|
+
# callbacks never read flow state written from this worker.
|
|
436
|
+
flow = self.flow
|
|
437
|
+
if flow is not None:
|
|
438
|
+
try:
|
|
439
|
+
perform_busy = getattr(flow, "perform_busy", None)
|
|
440
|
+
if callable(perform_busy):
|
|
441
|
+
perform_busy()
|
|
442
|
+
else:
|
|
443
|
+
flow.perform_save()
|
|
444
|
+
except BaseException as exc: # noqa: BLE001 - record, never hang the UI
|
|
445
|
+
set_busy_failure = getattr(flow, "set_busy_failure", None)
|
|
446
|
+
if callable(set_busy_failure):
|
|
447
|
+
set_busy_failure(f"{type(exc).__name__}: {exc}")
|
|
448
|
+
else:
|
|
449
|
+
flow.set_save_failure(f"{type(exc).__name__}: {exc}")
|
|
450
|
+
self._invalidate()
|
|
451
|
+
try:
|
|
452
|
+
loop = getattr(get_app(), "loop", None)
|
|
453
|
+
if loop is not None:
|
|
454
|
+
loop.call_soon_threadsafe(self._finalize_save)
|
|
455
|
+
else:
|
|
456
|
+
self._finalize_save()
|
|
457
|
+
except Exception:
|
|
458
|
+
self._finalize_save()
|
|
459
|
+
|
|
460
|
+
def _finalize_save(self) -> None:
|
|
461
|
+
flow = self.flow
|
|
462
|
+
if flow is not None:
|
|
463
|
+
apply_busy_outcome = getattr(flow, "apply_busy_outcome", None)
|
|
464
|
+
if callable(apply_busy_outcome):
|
|
465
|
+
apply_busy_outcome()
|
|
466
|
+
else:
|
|
467
|
+
flow.apply_save_outcome()
|
|
468
|
+
self._busy["running"] = False
|
|
469
|
+
self._pump()
|
|
470
|
+
|
|
471
|
+
# ----------------------------------------------------------- key handling
|
|
472
|
+
|
|
473
|
+
def _on_enter(self) -> None:
|
|
474
|
+
flow = self.flow
|
|
475
|
+
if flow is None:
|
|
476
|
+
return
|
|
477
|
+
mode = flow.current_mode()
|
|
478
|
+
if mode == "input":
|
|
479
|
+
flow.submit_input(self._input.text)
|
|
480
|
+
elif mode == "list":
|
|
481
|
+
flow.choose_current()
|
|
482
|
+
elif mode == "confirm":
|
|
483
|
+
flow.confirm(flow.screen().confirm_default)
|
|
484
|
+
self._pump()
|
|
485
|
+
|
|
486
|
+
def register(self, kb: KeyBindings) -> None:
|
|
487
|
+
is_open = self.open_condition
|
|
488
|
+
is_list = Condition(lambda: self._open["on"] and self._mode() == "list")
|
|
489
|
+
is_confirm = Condition(lambda: self._open["on"] and self._mode() == "confirm")
|
|
490
|
+
is_busy = Condition(lambda: self._open["on"] and self._mode() == "busy")
|
|
491
|
+
is_menu = Condition(
|
|
492
|
+
lambda: (
|
|
493
|
+
self._open["on"]
|
|
494
|
+
and self.flow is not None
|
|
495
|
+
and getattr(self.flow, "stage", "") == "menu"
|
|
496
|
+
)
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
@kb.add("enter", filter=is_open & ~is_busy, eager=True)
|
|
500
|
+
def _enter(event: Any) -> None:
|
|
501
|
+
self._on_enter()
|
|
502
|
+
|
|
503
|
+
@kb.add("escape", filter=is_open & ~is_busy)
|
|
504
|
+
def _escape(event: Any) -> None:
|
|
505
|
+
if self.flow is not None:
|
|
506
|
+
self.flow.back()
|
|
507
|
+
self._pump()
|
|
508
|
+
|
|
509
|
+
@kb.add("up", filter=is_list, eager=True)
|
|
510
|
+
@kb.add("k", filter=is_list, eager=True)
|
|
511
|
+
@kb.add("K", filter=is_list, eager=True)
|
|
512
|
+
def _up(event: Any) -> None:
|
|
513
|
+
if self.flow is not None:
|
|
514
|
+
self.flow.move(-1)
|
|
515
|
+
self._invalidate()
|
|
516
|
+
|
|
517
|
+
@kb.add("down", filter=is_list, eager=True)
|
|
518
|
+
@kb.add("j", filter=is_list, eager=True)
|
|
519
|
+
@kb.add("J", filter=is_list, eager=True)
|
|
520
|
+
def _down(event: Any) -> None:
|
|
521
|
+
if self.flow is not None:
|
|
522
|
+
self.flow.move(1)
|
|
523
|
+
self._invalidate()
|
|
524
|
+
|
|
525
|
+
for _digit in "123456789":
|
|
526
|
+
|
|
527
|
+
@kb.add(_digit, filter=is_list, eager=True)
|
|
528
|
+
def _pick(event: Any) -> None:
|
|
529
|
+
if self.flow is None:
|
|
530
|
+
return
|
|
531
|
+
try:
|
|
532
|
+
self.flow.choose_index(int(event.data) - 1)
|
|
533
|
+
except (TypeError, ValueError):
|
|
534
|
+
return
|
|
535
|
+
self._pump()
|
|
536
|
+
|
|
537
|
+
@kb.add("q", filter=(is_list | is_confirm), eager=True)
|
|
538
|
+
@kb.add("Q", filter=(is_list | is_confirm), eager=True)
|
|
539
|
+
def _q(event: Any) -> None:
|
|
540
|
+
if self.flow is not None:
|
|
541
|
+
self.flow.back()
|
|
542
|
+
self._pump()
|
|
543
|
+
|
|
544
|
+
@kb.add("s", filter=is_menu, eager=True)
|
|
545
|
+
@kb.add("S", filter=is_menu, eager=True)
|
|
546
|
+
def _save(event: Any) -> None:
|
|
547
|
+
if self.flow is not None:
|
|
548
|
+
self.flow._goto("saving")
|
|
549
|
+
self._pump()
|
|
550
|
+
|
|
551
|
+
@kb.add("y", filter=is_confirm, eager=True)
|
|
552
|
+
@kb.add("Y", filter=is_confirm, eager=True)
|
|
553
|
+
def _yes(event: Any) -> None:
|
|
554
|
+
if self.flow is not None:
|
|
555
|
+
self.flow.confirm(True)
|
|
556
|
+
self._pump()
|
|
557
|
+
|
|
558
|
+
@kb.add("n", filter=is_confirm, eager=True)
|
|
559
|
+
@kb.add("N", filter=is_confirm, eager=True)
|
|
560
|
+
def _no(event: Any) -> None:
|
|
561
|
+
if self.flow is not None:
|
|
562
|
+
self.flow.confirm(False)
|
|
563
|
+
self._pump()
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
__all__ = ["ConfigOverlay"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Static text + the model-name prettifier for the TUI welcome/footer.
|
|
2
|
+
|
|
3
|
+
Kept as pure functions so they can be unit-tested without constructing the
|
|
4
|
+
prompt_toolkit application.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from prompt_toolkit.formatted_text import FormattedText
|
|
12
|
+
|
|
13
|
+
HEADING_TEXT = "Alysis Code" # plain wordmark fallback (narrow terminals)
|
|
14
|
+
CREDIT_TEXT = "crafted by AlysisAI"
|
|
15
|
+
HINT_TEXT = "/forge for an autonomous run · tab to switch persona · /help for everything"
|
|
16
|
+
# Welcome screen: full greeting. Once the conversation is underway the input
|
|
17
|
+
# switches to the shorter follow-up placeholder below.
|
|
18
|
+
INPUT_PLACEHOLDER = "I'm Alysis Code, your coding buddy — how can I help you?"
|
|
19
|
+
INPUT_PLACEHOLDER_FOLLOWUP = "Message Alysis Code…"
|
|
20
|
+
# Forge planning session: nudge the forge verbs (and that plain text becomes a
|
|
21
|
+
# requirement) so the input reads as the plan editor it now is.
|
|
22
|
+
INPUT_PLACEHOLDER_FORGE = "Forge — /goal · /task · /show · /execute plan · /done"
|
|
23
|
+
|
|
24
|
+
# Tokens that should keep a specific casing instead of naive title-casing.
|
|
25
|
+
_ACRONYMS = {
|
|
26
|
+
"ai": "AI",
|
|
27
|
+
"deepseek": "DeepSeek",
|
|
28
|
+
"glm": "GLM",
|
|
29
|
+
"gpt": "GPT",
|
|
30
|
+
"llm": "LLM",
|
|
31
|
+
"mimo": "MiMo",
|
|
32
|
+
"openai": "OpenAI",
|
|
33
|
+
"qwen": "Qwen",
|
|
34
|
+
"xai": "xAI",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def pretty_model_label(model: str | None) -> str:
|
|
39
|
+
"""Turn a raw model id into a friendly footer label.
|
|
40
|
+
|
|
41
|
+
``deepseek-chat`` -> ``DeepSeek Chat``; ``gpt-4o`` -> ``GPT 4o``. Falls back
|
|
42
|
+
to the raw id when there is nothing sensible to do.
|
|
43
|
+
"""
|
|
44
|
+
raw = (model or "").strip()
|
|
45
|
+
if not raw:
|
|
46
|
+
return "model"
|
|
47
|
+
name = raw.rsplit("/", 1)[-1]
|
|
48
|
+
tokens = [t for t in re.split(r"[-_\s]+", name) if t]
|
|
49
|
+
out: list[str] = []
|
|
50
|
+
for token in tokens:
|
|
51
|
+
low = token.lower()
|
|
52
|
+
if low in _ACRONYMS:
|
|
53
|
+
out.append(_ACRONYMS[low])
|
|
54
|
+
elif token.isupper():
|
|
55
|
+
out.append(token)
|
|
56
|
+
else:
|
|
57
|
+
out.append(token[:1].upper() + token[1:])
|
|
58
|
+
return " ".join(out) or raw
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def heading_fragments() -> FormattedText:
|
|
62
|
+
return FormattedText([("class:tui.heading", HEADING_TEXT)])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def hint_fragments() -> FormattedText:
|
|
66
|
+
return FormattedText([("class:tui.hint", HINT_TEXT)])
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"HEADING_TEXT",
|
|
71
|
+
"HINT_TEXT",
|
|
72
|
+
"INPUT_PLACEHOLDER",
|
|
73
|
+
"INPUT_PLACEHOLDER_FOLLOWUP",
|
|
74
|
+
"INPUT_PLACEHOLDER_FORGE",
|
|
75
|
+
"heading_fragments",
|
|
76
|
+
"hint_fragments",
|
|
77
|
+
"pretty_model_label",
|
|
78
|
+
]
|