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,1248 @@
|
|
|
1
|
+
# ruff: noqa: F401,F403,F405,I001
|
|
2
|
+
# Legacy split module: dependencies are synced by cli_surface.py.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .cli_common import *
|
|
6
|
+
from .update import _cached_update_status_summary
|
|
7
|
+
from ...config import resolve_web_search_policy
|
|
8
|
+
from ...personas import normalize_persona, persona_modes_enabled
|
|
9
|
+
from ...llm_error_display import classify_llm_error_display, friendly_llm_error_message
|
|
10
|
+
from ...llm.protocols import OPENAI_COMPAT_PROTOCOL, get_provider_protocol_capabilities
|
|
11
|
+
from ...profiles import get_active_profile, resolve_effective_base_url
|
|
12
|
+
from ...provider_telemetry import last_provider_call_summary, last_web_search_summary
|
|
13
|
+
from ...step_budget import (
|
|
14
|
+
DEFAULT_CHAT_MAX_STEPS,
|
|
15
|
+
DEFAULT_SUBAGENT_MAX_STEPS,
|
|
16
|
+
DEFAULT_TASK_MAX_STEPS,
|
|
17
|
+
normalize_step_budget_policy,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_CHAT_MODE_STATUS_LABELS = {
|
|
22
|
+
"review": "safe (review)",
|
|
23
|
+
"auto": "fast (auto)",
|
|
24
|
+
"readonly": "read (readonly)",
|
|
25
|
+
"fullaccess": "full (fullaccess)",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _chat_mode_status_label(mode: Any) -> str:
|
|
30
|
+
normalized = str(mode or "").strip().lower()
|
|
31
|
+
return _CHAT_MODE_STATUS_LABELS.get(normalized, normalized or "unknown")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _session_execution_policy(session: Any) -> str:
|
|
35
|
+
return normalize_step_budget_policy(
|
|
36
|
+
getattr(
|
|
37
|
+
session,
|
|
38
|
+
"step_budget_policy",
|
|
39
|
+
getattr(getattr(session, "cfg", None), "step_budget_policy", "autonomous"),
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _session_step_limit_label(session: Any, *, scope: str) -> str:
|
|
45
|
+
policy = _session_execution_policy(session)
|
|
46
|
+
if scope == "chat":
|
|
47
|
+
explicit = getattr(session, "chat_turn_fixed_override", None)
|
|
48
|
+
if isinstance(explicit, int) and explicit > 0:
|
|
49
|
+
return str(explicit)
|
|
50
|
+
if policy == "autonomous":
|
|
51
|
+
return "unlimited"
|
|
52
|
+
field = {
|
|
53
|
+
"chat": "max_steps",
|
|
54
|
+
"task": "task_max_steps",
|
|
55
|
+
"subagent": "subagent_max_steps",
|
|
56
|
+
}[scope]
|
|
57
|
+
default = {
|
|
58
|
+
"chat": DEFAULT_CHAT_MAX_STEPS,
|
|
59
|
+
"task": DEFAULT_TASK_MAX_STEPS,
|
|
60
|
+
"subagent": DEFAULT_SUBAGENT_MAX_STEPS,
|
|
61
|
+
}[scope]
|
|
62
|
+
return str(getattr(session, field, getattr(getattr(session, "cfg", None), field, default)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _active_step_limit_label(session: Any) -> str:
|
|
66
|
+
runtime = getattr(session, "step_budget_runtime", None)
|
|
67
|
+
active = getattr(runtime, "active_turn_budget", None)
|
|
68
|
+
if isinstance(active, int) and active > 0:
|
|
69
|
+
return str(active)
|
|
70
|
+
resolution = getattr(runtime, "last_resolution", None)
|
|
71
|
+
if resolution is not None and bool(getattr(resolution, "unlimited", False)):
|
|
72
|
+
return "unlimited"
|
|
73
|
+
resolved = getattr(resolution, "resolved_max_steps", None)
|
|
74
|
+
if isinstance(resolved, int) and resolved > 0:
|
|
75
|
+
return str(resolved)
|
|
76
|
+
return _session_step_limit_label(session, scope="chat")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _print_chat_status(
|
|
80
|
+
*,
|
|
81
|
+
console: Console,
|
|
82
|
+
session: Any,
|
|
83
|
+
pending_images: list[str],
|
|
84
|
+
) -> None:
|
|
85
|
+
model = getattr(getattr(session, "client", None), "model", "?")
|
|
86
|
+
temperature = getattr(getattr(session, "client", None), "temperature", "?")
|
|
87
|
+
stream = bool(getattr(session, "stream", False))
|
|
88
|
+
mode = getattr(session, "mode", "?")
|
|
89
|
+
root = Path(getattr(session, "root", Path(".")))
|
|
90
|
+
branch = _current_branch_label(root)
|
|
91
|
+
cfg_obj = getattr(session, "cfg", None)
|
|
92
|
+
resolved_cfg = cfg_obj if isinstance(cfg_obj, AppConfig) else None
|
|
93
|
+
session_api_key = (
|
|
94
|
+
str(getattr(getattr(session, "client", None), "api_key", "") or "").strip() or None
|
|
95
|
+
)
|
|
96
|
+
web_search_status = resolve_web_search_runtime_status(
|
|
97
|
+
cfg=resolved_cfg,
|
|
98
|
+
api_key=session_api_key,
|
|
99
|
+
)
|
|
100
|
+
active_protocol, streaming_supported = _active_protocol_status(
|
|
101
|
+
cfg=resolved_cfg,
|
|
102
|
+
client=getattr(session, "client", None),
|
|
103
|
+
)
|
|
104
|
+
table = _Table(title="Chat Status")
|
|
105
|
+
table.add_column("setting")
|
|
106
|
+
table.add_column("value", overflow="fold")
|
|
107
|
+
table.add_row("mode", _chat_mode_status_label(mode))
|
|
108
|
+
if persona_modes_enabled(getattr(session, "cfg", None)):
|
|
109
|
+
table.add_row(
|
|
110
|
+
"persona",
|
|
111
|
+
normalize_persona(
|
|
112
|
+
getattr(session, "persona", "code"),
|
|
113
|
+
getattr(session, "persona_registry", None),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
table.add_row("trace", _chat_trace_level(session))
|
|
117
|
+
table.add_row("model", str(model))
|
|
118
|
+
table.add_row("protocol", active_protocol)
|
|
119
|
+
table.add_row(
|
|
120
|
+
"api_key_source",
|
|
121
|
+
_api_key_source_label(str(getattr(session, "api_key_source", "missing") or "missing")),
|
|
122
|
+
)
|
|
123
|
+
table.add_row("temperature", str(temperature))
|
|
124
|
+
table.add_row("stream", "on" if stream else "off")
|
|
125
|
+
table.add_row("streaming_supported", streaming_supported)
|
|
126
|
+
table.add_row(
|
|
127
|
+
"subagents",
|
|
128
|
+
"on" if bool(getattr(session, "subagents_enabled", False)) else "off",
|
|
129
|
+
)
|
|
130
|
+
table.add_row(
|
|
131
|
+
"execution_policy",
|
|
132
|
+
_session_execution_policy(session),
|
|
133
|
+
)
|
|
134
|
+
table.add_row("chat_step_limit", _session_step_limit_label(session, scope="chat"))
|
|
135
|
+
table.add_row("task_step_limit", _session_step_limit_label(session, scope="task"))
|
|
136
|
+
table.add_row("subagent_step_limit", _session_step_limit_label(session, scope="subagent"))
|
|
137
|
+
table.add_row("active_step_limit", _active_step_limit_label(session))
|
|
138
|
+
table.add_row("session", str(getattr(getattr(session, "store", None), "session_id", "-")))
|
|
139
|
+
active_workdir = Path(resolve_session_active_workdir_path(session))
|
|
140
|
+
focus_dir = Path(getattr(session, "focus_dir", root) or root)
|
|
141
|
+
focus_relpath = str(getattr(session, "focus_relpath", ".") or ".")
|
|
142
|
+
active_workdir_relpath = str(resolve_session_active_workdir_relpath(session) or ".")
|
|
143
|
+
table.add_row("workspace_root", os.fspath(root))
|
|
144
|
+
table.add_row("focus_dir", os.fspath(focus_dir))
|
|
145
|
+
table.add_row("focus_relpath", focus_relpath)
|
|
146
|
+
table.add_row("active_workdir", os.fspath(active_workdir))
|
|
147
|
+
table.add_row("active_workdir_relpath", active_workdir_relpath)
|
|
148
|
+
table.add_row("branch", branch)
|
|
149
|
+
table.add_row("dirty", "yes" if _is_git_dirty(root) else "no")
|
|
150
|
+
table.add_row("update", _cached_update_status_summary(resolved_cfg))
|
|
151
|
+
table.add_row("task", "-")
|
|
152
|
+
table.add_row("queued_images", str(len(pending_images)))
|
|
153
|
+
web_search_policy = resolve_web_search_policy(resolved_cfg)
|
|
154
|
+
web_search_ready = web_search_status.registration_ready and web_search_policy != "off"
|
|
155
|
+
web_search_label = (
|
|
156
|
+
"policy-disabled" if web_search_policy == "off" else web_search_status.availability_label
|
|
157
|
+
)
|
|
158
|
+
table.add_row("web_search", web_search_label)
|
|
159
|
+
table.add_row("web_search_policy", web_search_policy)
|
|
160
|
+
table.add_row("web_search_mode", web_search_status.mode)
|
|
161
|
+
table.add_row("web_search_provider", web_search_status.provider or "(none)")
|
|
162
|
+
table.add_row(
|
|
163
|
+
"web_search_registration",
|
|
164
|
+
"yes" if web_search_ready else "no",
|
|
165
|
+
)
|
|
166
|
+
if web_search_status.provider not in {None, "tavily"}:
|
|
167
|
+
table.add_row("web_search_base_url", web_search_status.base_url or "(missing)")
|
|
168
|
+
table.add_row("web_search_model", web_search_status.model or "(missing)")
|
|
169
|
+
table.add_row(
|
|
170
|
+
"web_search_api_key",
|
|
171
|
+
"available" if web_search_status.api_key_available else "missing",
|
|
172
|
+
)
|
|
173
|
+
table.add_row("web_search_note", web_search_status.summary)
|
|
174
|
+
table.add_row(
|
|
175
|
+
"web_search_setup",
|
|
176
|
+
(
|
|
177
|
+
"Set `alysis config set web_search_policy auto` to expose search to the model."
|
|
178
|
+
if web_search_policy == "off"
|
|
179
|
+
else web_search_status.setup_hint
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
last_call = last_provider_call_summary()
|
|
183
|
+
if last_call:
|
|
184
|
+
table.add_row(
|
|
185
|
+
"last_provider_call",
|
|
186
|
+
(
|
|
187
|
+
f"{last_call.get('provider_key') or '?'} / "
|
|
188
|
+
f"{last_call.get('protocol') or '?'} / "
|
|
189
|
+
f"{last_call.get('status_category') or '?'} / "
|
|
190
|
+
f"{last_call.get('latency_ms') or 0}ms"
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
stream_summary = last_call.get("streaming") if isinstance(last_call, dict) else None
|
|
194
|
+
if isinstance(stream_summary, dict):
|
|
195
|
+
table.add_row(
|
|
196
|
+
"last_stream",
|
|
197
|
+
(
|
|
198
|
+
f"events={stream_summary.get('event_count') or 0}, "
|
|
199
|
+
f"deltas={stream_summary.get('text_delta_count') or 0}, "
|
|
200
|
+
f"first_token_ms={stream_summary.get('first_token_latency_ms')}"
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
last_search = last_web_search_summary()
|
|
204
|
+
if last_search:
|
|
205
|
+
table.add_row(
|
|
206
|
+
"last_web_search",
|
|
207
|
+
(
|
|
208
|
+
f"{last_search.get('web_search_mode') or '?'} / "
|
|
209
|
+
f"{last_search.get('web_search_adapter') or '?'} / "
|
|
210
|
+
f"sources={last_search.get('source_count') or 0}"
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
registry = getattr(session, "model_registry", None)
|
|
214
|
+
resolved_model_name = str(model).strip()
|
|
215
|
+
if registry is not None and resolved_model_name:
|
|
216
|
+
try:
|
|
217
|
+
meta = registry.get(resolved_model_name)
|
|
218
|
+
except Exception as e: # noqa: BLE001
|
|
219
|
+
table.add_row("model_metadata_source", "error")
|
|
220
|
+
table.add_row("model_metadata_error", str(e))
|
|
221
|
+
else:
|
|
222
|
+
table.add_row("model_metadata_source", str(getattr(meta, "source", "unknown")))
|
|
223
|
+
table.add_row(
|
|
224
|
+
"model_metadata_error",
|
|
225
|
+
str(getattr(registry, "last_error", None) or "-"),
|
|
226
|
+
)
|
|
227
|
+
table.add_row(
|
|
228
|
+
"model_metadata_warning",
|
|
229
|
+
"; ".join(getattr(meta, "warnings", ())[:3]) or "-",
|
|
230
|
+
)
|
|
231
|
+
field_sources = getattr(meta, "field_sources", {}) or {}
|
|
232
|
+
table.add_row(
|
|
233
|
+
"context_window_source",
|
|
234
|
+
str(field_sources.get("context_window_tokens", "unknown")),
|
|
235
|
+
)
|
|
236
|
+
table.add_row(
|
|
237
|
+
"max_output_source",
|
|
238
|
+
str(field_sources.get("max_output_tokens", "unknown")),
|
|
239
|
+
)
|
|
240
|
+
for key, value in _bundled_catalog_provenance_rows(meta=meta, registry=registry):
|
|
241
|
+
table.add_row(key, value)
|
|
242
|
+
console.print(table)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _active_protocol_status(*, cfg: AppConfig | None, client: Any) -> tuple[str, str]:
|
|
246
|
+
protocol = str(getattr(client, "protocol", "") or "").strip()
|
|
247
|
+
provider_key = str(getattr(client, "provider_key", "") or "").strip()
|
|
248
|
+
if cfg is not None:
|
|
249
|
+
try:
|
|
250
|
+
profile = get_active_profile(cfg)
|
|
251
|
+
protocol = protocol or str(profile.protocol or OPENAI_COMPAT_PROTOCOL).strip()
|
|
252
|
+
provider_key = provider_key or str(profile.name or "").strip()
|
|
253
|
+
base_url = resolve_effective_base_url(cfg=cfg, profile=profile)
|
|
254
|
+
except Exception: # noqa: BLE001
|
|
255
|
+
base_url = str(getattr(client, "base_url", "") or "")
|
|
256
|
+
else:
|
|
257
|
+
base_url = str(getattr(client, "base_url", "") or "")
|
|
258
|
+
protocol = protocol or OPENAI_COMPAT_PROTOCOL
|
|
259
|
+
capabilities = get_provider_protocol_capabilities(
|
|
260
|
+
provider_key=provider_key,
|
|
261
|
+
protocol=protocol,
|
|
262
|
+
)
|
|
263
|
+
if capabilities is not None:
|
|
264
|
+
supported = "yes" if capabilities.supports_streaming else "no"
|
|
265
|
+
else:
|
|
266
|
+
supported = "yes" if protocol == OPENAI_COMPAT_PROTOCOL else "unknown"
|
|
267
|
+
if not provider_key and base_url:
|
|
268
|
+
provider_key = base_url
|
|
269
|
+
return protocol, supported
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _chat_status_panel_spec(*, session: Any, pending_images: list[str]) -> dict[str, Any]:
|
|
273
|
+
"""Build the structured spec for the TUI ``/status`` popup.
|
|
274
|
+
|
|
275
|
+
Mirrors :func:`_print_chat_status` (same getters) but returns grouped
|
|
276
|
+
key/value sections with a health ``tone`` per value instead of a flat Rich
|
|
277
|
+
table, so the full-screen TUI can render it as a centered panel. ``tone`` is
|
|
278
|
+
one of "accent" (on/healthy, green), "plain", "warn", "err", "dim". Green is
|
|
279
|
+
the only accent; degraded states stay "plain", real failures use "err".
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def tone(flag: bool, true_tone: str = "accent", false_tone: str = "plain") -> str:
|
|
283
|
+
return true_tone if flag else false_tone
|
|
284
|
+
|
|
285
|
+
model = getattr(getattr(session, "client", None), "model", "?")
|
|
286
|
+
temperature = getattr(getattr(session, "client", None), "temperature", "?")
|
|
287
|
+
stream = bool(getattr(session, "stream", False))
|
|
288
|
+
mode = str(getattr(session, "mode", "?"))
|
|
289
|
+
root = Path(getattr(session, "root", Path(".")))
|
|
290
|
+
branch = _current_branch_label(root)
|
|
291
|
+
dirty = _is_git_dirty(root)
|
|
292
|
+
subagents = bool(getattr(session, "subagents_enabled", False))
|
|
293
|
+
cfg_obj = getattr(session, "cfg", None)
|
|
294
|
+
resolved_cfg = cfg_obj if isinstance(cfg_obj, AppConfig) else None
|
|
295
|
+
session_api_key = (
|
|
296
|
+
str(getattr(getattr(session, "client", None), "api_key", "") or "").strip() or None
|
|
297
|
+
)
|
|
298
|
+
web = resolve_web_search_runtime_status(cfg=resolved_cfg, api_key=session_api_key)
|
|
299
|
+
|
|
300
|
+
def _attr(name: str, default: Any) -> Any:
|
|
301
|
+
return getattr(session, name, getattr(getattr(session, "cfg", None), name, default))
|
|
302
|
+
|
|
303
|
+
session_rows: list[tuple[str, str, str]] = [
|
|
304
|
+
("mode", _chat_mode_status_label(mode), "accent"),
|
|
305
|
+
*(
|
|
306
|
+
[
|
|
307
|
+
(
|
|
308
|
+
"persona",
|
|
309
|
+
normalize_persona(
|
|
310
|
+
getattr(session, "persona", "code"),
|
|
311
|
+
getattr(session, "persona_registry", None),
|
|
312
|
+
),
|
|
313
|
+
"accent",
|
|
314
|
+
)
|
|
315
|
+
]
|
|
316
|
+
if persona_modes_enabled(getattr(session, "cfg", None))
|
|
317
|
+
else []
|
|
318
|
+
),
|
|
319
|
+
("model", str(model), "accent"),
|
|
320
|
+
("trace", _chat_trace_level(session), "plain"),
|
|
321
|
+
("session", str(getattr(getattr(session, "store", None), "session_id", "-")), "dim"),
|
|
322
|
+
("branch", branch, "plain"),
|
|
323
|
+
("dirty", "yes" if dirty else "no", tone(not dirty)),
|
|
324
|
+
("update", _cached_update_status_summary(resolved_cfg), "plain"),
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
model_rows: list[tuple[str, str, str]] = [
|
|
328
|
+
(
|
|
329
|
+
"api_key_source",
|
|
330
|
+
_api_key_source_label(str(getattr(session, "api_key_source", "missing") or "missing")),
|
|
331
|
+
"plain",
|
|
332
|
+
),
|
|
333
|
+
("temperature", str(temperature), "plain"),
|
|
334
|
+
("stream", "on" if stream else "off", tone(stream)),
|
|
335
|
+
("subagents", "on" if subagents else "off", tone(subagents)),
|
|
336
|
+
("execution_policy", _session_execution_policy(session), "plain"),
|
|
337
|
+
("chat_step_limit", _session_step_limit_label(session, scope="chat"), "plain"),
|
|
338
|
+
("task_step_limit", _session_step_limit_label(session, scope="task"), "plain"),
|
|
339
|
+
(
|
|
340
|
+
"subagent_step_limit",
|
|
341
|
+
_session_step_limit_label(session, scope="subagent"),
|
|
342
|
+
"plain",
|
|
343
|
+
),
|
|
344
|
+
]
|
|
345
|
+
model_rows.append(
|
|
346
|
+
(
|
|
347
|
+
"active_step_limit",
|
|
348
|
+
_active_step_limit_label(session),
|
|
349
|
+
"plain",
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
active_workdir = Path(resolve_session_active_workdir_path(session))
|
|
354
|
+
focus_dir = Path(getattr(session, "focus_dir", root) or root)
|
|
355
|
+
workspace_rows: list[tuple[str, str, str]] = [
|
|
356
|
+
("workspace_root", os.fspath(root), "plain"),
|
|
357
|
+
("focus_dir", os.fspath(focus_dir), "plain"),
|
|
358
|
+
("focus_relpath", str(getattr(session, "focus_relpath", ".") or "."), "plain"),
|
|
359
|
+
("active_workdir", os.fspath(active_workdir), "accent"),
|
|
360
|
+
(
|
|
361
|
+
"active_workdir_relpath",
|
|
362
|
+
str(resolve_session_active_workdir_relpath(session) or "."),
|
|
363
|
+
"plain",
|
|
364
|
+
),
|
|
365
|
+
("queued_images", str(len(pending_images)), "plain"),
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
web_policy = resolve_web_search_policy(resolved_cfg)
|
|
369
|
+
web_ready = web.registration_ready and web_policy != "off"
|
|
370
|
+
web_label = "policy-disabled" if web_policy == "off" else web.availability_label
|
|
371
|
+
web_unavailable = not web_ready
|
|
372
|
+
web_rows: list[tuple[str, str, str]] = [
|
|
373
|
+
(
|
|
374
|
+
"status",
|
|
375
|
+
web_label,
|
|
376
|
+
"err" if web_unavailable else tone(web_ready),
|
|
377
|
+
),
|
|
378
|
+
("policy", web_policy, "plain"),
|
|
379
|
+
("mode", web.mode, "plain"),
|
|
380
|
+
("provider", web.provider or "(none)", "plain"),
|
|
381
|
+
("registration", "yes" if web_ready else "no", tone(web_ready)),
|
|
382
|
+
]
|
|
383
|
+
if web.provider not in {None, "tavily"}:
|
|
384
|
+
web_rows.append(("base_url", web.base_url or "(missing)", "plain"))
|
|
385
|
+
web_rows.append(("model", web.model or "(missing)", "plain"))
|
|
386
|
+
web_rows.append(
|
|
387
|
+
(
|
|
388
|
+
"api_key",
|
|
389
|
+
"available" if web.api_key_available else "missing",
|
|
390
|
+
tone(web.api_key_available),
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
web_rows.append(("note", web.summary, "plain"))
|
|
394
|
+
web_rows.append(
|
|
395
|
+
(
|
|
396
|
+
"setup",
|
|
397
|
+
(
|
|
398
|
+
"Set `alysis config set web_search_policy auto` to expose search to the model."
|
|
399
|
+
if web_policy == "off"
|
|
400
|
+
else web.setup_hint
|
|
401
|
+
),
|
|
402
|
+
"plain",
|
|
403
|
+
)
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
sections: list[tuple[str, list[tuple[str, str, str]]]] = [
|
|
407
|
+
("Session", session_rows),
|
|
408
|
+
("Model", model_rows),
|
|
409
|
+
("Workspace", workspace_rows),
|
|
410
|
+
("Web search", web_rows),
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
registry = getattr(session, "model_registry", None)
|
|
414
|
+
resolved_model_name = str(model).strip()
|
|
415
|
+
if registry is not None and resolved_model_name:
|
|
416
|
+
meta_rows: list[tuple[str, str, str]] = []
|
|
417
|
+
try:
|
|
418
|
+
meta = registry.get(resolved_model_name)
|
|
419
|
+
except Exception as e: # noqa: BLE001
|
|
420
|
+
meta_rows.append(("source", "error", "err"))
|
|
421
|
+
meta_rows.append(("error", str(e), "err"))
|
|
422
|
+
else:
|
|
423
|
+
meta_rows.append(("source", str(getattr(meta, "source", "unknown")), "plain"))
|
|
424
|
+
last_error = str(getattr(registry, "last_error", None) or "-")
|
|
425
|
+
meta_rows.append(("error", last_error, "err" if last_error != "-" else "plain"))
|
|
426
|
+
warning = "; ".join(getattr(meta, "warnings", ())[:3]) or "-"
|
|
427
|
+
meta_rows.append(("warning", warning, "warn" if warning != "-" else "plain"))
|
|
428
|
+
field_sources = getattr(meta, "field_sources", {}) or {}
|
|
429
|
+
meta_rows.append(
|
|
430
|
+
(
|
|
431
|
+
"context_window_source",
|
|
432
|
+
str(field_sources.get("context_window_tokens", "unknown")),
|
|
433
|
+
"plain",
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
meta_rows.append(
|
|
437
|
+
(
|
|
438
|
+
"max_output_source",
|
|
439
|
+
str(field_sources.get("max_output_tokens", "unknown")),
|
|
440
|
+
"plain",
|
|
441
|
+
)
|
|
442
|
+
)
|
|
443
|
+
for key, value in _bundled_catalog_provenance_rows(meta=meta, registry=registry):
|
|
444
|
+
meta_rows.append((key, value, "dim"))
|
|
445
|
+
sections.append(("Model metadata", meta_rows))
|
|
446
|
+
|
|
447
|
+
return {"title": "Session Status", "sections": sections}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _print_chat_pwd(*, console: Console, session: Any) -> None:
|
|
451
|
+
root = Path(getattr(session, "root", Path("."))).resolve()
|
|
452
|
+
focus_dir = Path(getattr(session, "focus_dir", root) or root)
|
|
453
|
+
focus_relpath = str(getattr(session, "focus_relpath", ".") or ".")
|
|
454
|
+
active_workdir = Path(resolve_session_active_workdir_path(session))
|
|
455
|
+
active_workdir_relpath = str(resolve_session_active_workdir_relpath(session) or ".")
|
|
456
|
+
console.print(f"active_workdir: {active_workdir}")
|
|
457
|
+
console.print(f"active_workdir_relpath: {active_workdir_relpath}")
|
|
458
|
+
console.print(f"focus_dir: {focus_dir}")
|
|
459
|
+
console.print(f"focus_relpath: {focus_relpath}")
|
|
460
|
+
console.print(f"workspace_root: {root}")
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _model_metadata_uses_bundled_catalog(*, meta: Any, registry: Any) -> bool:
|
|
464
|
+
if str(getattr(meta, "source", "")).strip() == BUNDLED_MODEL_CATALOG_SOURCE:
|
|
465
|
+
return True
|
|
466
|
+
field_sources = getattr(meta, "field_sources", {}) or {}
|
|
467
|
+
if any(
|
|
468
|
+
str(source).strip() == BUNDLED_MODEL_CATALOG_SOURCE for source in field_sources.values()
|
|
469
|
+
):
|
|
470
|
+
return True
|
|
471
|
+
last_error = str(getattr(registry, "last_error", "") or "").strip().casefold()
|
|
472
|
+
return "bundled model catalog" in last_error
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _bundled_catalog_provenance_rows(*, meta: Any, registry: Any) -> list[tuple[str, str]]:
|
|
476
|
+
if not _model_metadata_uses_bundled_catalog(meta=meta, registry=registry):
|
|
477
|
+
return []
|
|
478
|
+
provenance = _patchable(
|
|
479
|
+
"get_bundled_model_catalog_provenance",
|
|
480
|
+
get_bundled_model_catalog_provenance,
|
|
481
|
+
)()
|
|
482
|
+
if provenance.error is not None:
|
|
483
|
+
return [("bundled_catalog_provenance", provenance.error)]
|
|
484
|
+
rows: list[tuple[str, str]] = []
|
|
485
|
+
if provenance.upstream_commit_sha is not None:
|
|
486
|
+
rows.append(("bundled_catalog_commit", provenance.upstream_commit_sha))
|
|
487
|
+
if provenance.fetched_at_utc is not None:
|
|
488
|
+
rows.append(("bundled_catalog_fetched_at", provenance.fetched_at_utc))
|
|
489
|
+
return rows
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _print_chat_usage(*, console: Console, session: Any) -> None:
|
|
493
|
+
summary = getattr(session, "usage_summary", None)
|
|
494
|
+
if summary is None:
|
|
495
|
+
console.print("Usage tracking unavailable for this session.")
|
|
496
|
+
return
|
|
497
|
+
rows = summary.by_model_rows()
|
|
498
|
+
if not rows:
|
|
499
|
+
console.print("No usage events yet in this session.")
|
|
500
|
+
return
|
|
501
|
+
table = _Table(title="Usage")
|
|
502
|
+
table.add_column("model")
|
|
503
|
+
table.add_column("input total", justify="right")
|
|
504
|
+
table.add_column("uncached", justify="right")
|
|
505
|
+
table.add_column("cache read", justify="right")
|
|
506
|
+
table.add_column("cache write", justify="right")
|
|
507
|
+
table.add_column("output", justify="right")
|
|
508
|
+
table.add_column("total", justify="right")
|
|
509
|
+
table.add_column("cost", justify="right")
|
|
510
|
+
table.add_column("source", justify="right")
|
|
511
|
+
for row in rows:
|
|
512
|
+
cost_display = _format_usage_billing_for_display(row)
|
|
513
|
+
table.add_row(
|
|
514
|
+
str(row.get("model") or "-"),
|
|
515
|
+
_format_exact_token_count(row.get("prompt_tokens")),
|
|
516
|
+
_format_reported_token_total(
|
|
517
|
+
row,
|
|
518
|
+
token_key="input_tokens_uncached",
|
|
519
|
+
reported_calls_key="input_tokens_uncached_reported_calls",
|
|
520
|
+
derived_calls_key="input_tokens_uncached_derived_calls",
|
|
521
|
+
),
|
|
522
|
+
_format_reported_token_total(
|
|
523
|
+
row,
|
|
524
|
+
token_key="cache_read_input_tokens",
|
|
525
|
+
reported_calls_key="cache_read_reported_calls",
|
|
526
|
+
),
|
|
527
|
+
_format_reported_token_total(
|
|
528
|
+
row,
|
|
529
|
+
token_key="cache_creation_input_tokens",
|
|
530
|
+
reported_calls_key="cache_creation_reported_calls",
|
|
531
|
+
),
|
|
532
|
+
_format_exact_token_count(row.get("completion_tokens")),
|
|
533
|
+
_format_exact_token_count(row.get("total_tokens")),
|
|
534
|
+
cost_display,
|
|
535
|
+
_format_usage_source_for_display(row),
|
|
536
|
+
)
|
|
537
|
+
totals = summary.totals()
|
|
538
|
+
total_cost = _format_usage_billing_for_display(totals)
|
|
539
|
+
unknown_total = int(totals.get("unknown_cost_calls") or 0)
|
|
540
|
+
table.add_row(
|
|
541
|
+
"TOTAL",
|
|
542
|
+
_format_exact_token_count(totals.get("prompt_tokens")),
|
|
543
|
+
_format_reported_token_total(
|
|
544
|
+
totals,
|
|
545
|
+
token_key="input_tokens_uncached",
|
|
546
|
+
reported_calls_key="input_tokens_uncached_reported_calls",
|
|
547
|
+
derived_calls_key="input_tokens_uncached_derived_calls",
|
|
548
|
+
),
|
|
549
|
+
_format_reported_token_total(
|
|
550
|
+
totals,
|
|
551
|
+
token_key="cache_read_input_tokens",
|
|
552
|
+
reported_calls_key="cache_read_reported_calls",
|
|
553
|
+
),
|
|
554
|
+
_format_reported_token_total(
|
|
555
|
+
totals,
|
|
556
|
+
token_key="cache_creation_input_tokens",
|
|
557
|
+
reported_calls_key="cache_creation_reported_calls",
|
|
558
|
+
),
|
|
559
|
+
_format_exact_token_count(totals.get("completion_tokens")),
|
|
560
|
+
_format_exact_token_count(totals.get("total_tokens")),
|
|
561
|
+
total_cost,
|
|
562
|
+
_format_usage_source_for_display(totals),
|
|
563
|
+
)
|
|
564
|
+
console.print(table)
|
|
565
|
+
cache_efficiency = totals.get("cache_efficiency")
|
|
566
|
+
if isinstance(cache_efficiency, dict) and cache_efficiency.get("hit_ratio") is not None:
|
|
567
|
+
largest = cache_efficiency.get("largest_uncached_call")
|
|
568
|
+
largest_text = ""
|
|
569
|
+
if isinstance(largest, dict):
|
|
570
|
+
largest_text = (
|
|
571
|
+
"; largest uncached "
|
|
572
|
+
f"{_format_exact_token_count(largest.get('uncached_prompt_tokens'))} "
|
|
573
|
+
f"at call {int(largest.get('call_position') or 0)}"
|
|
574
|
+
)
|
|
575
|
+
console.print(
|
|
576
|
+
"[dim]Cache efficiency: "
|
|
577
|
+
f"{float(cache_efficiency['hit_ratio']) * 100:.2f}% hit "
|
|
578
|
+
f"({_format_exact_token_count(cache_efficiency.get('cached_prompt_tokens'))} cached, "
|
|
579
|
+
f"{_format_exact_token_count(cache_efficiency.get('uncached_prompt_tokens'))} uncached)"
|
|
580
|
+
f"{largest_text}.[/dim]"
|
|
581
|
+
)
|
|
582
|
+
console.print(
|
|
583
|
+
"[dim]Processed tokens are cumulative input + output across calls; they are not the "
|
|
584
|
+
"current context-window size. Cache read/write are input subdivisions and appear only "
|
|
585
|
+
"when the provider reports them. A ~ dollar amount is a catalog estimate, not an invoice.[/dim]"
|
|
586
|
+
)
|
|
587
|
+
if unknown_total > 0:
|
|
588
|
+
console.print(
|
|
589
|
+
"[yellow]Total cost is partial:[/yellow] "
|
|
590
|
+
f"{unknown_total} call(s) have unknown cost because pricing metadata is missing."
|
|
591
|
+
)
|
|
592
|
+
cache_cost_unknown = int(totals.get("cache_cost_pricing_missing_calls") or 0)
|
|
593
|
+
if cache_cost_unknown > 0:
|
|
594
|
+
console.print(
|
|
595
|
+
"[yellow]Cache pricing omitted:[/yellow] "
|
|
596
|
+
f"{cache_cost_unknown} cached call(s) need provider-specific read/write prices."
|
|
597
|
+
)
|
|
598
|
+
if int(totals.get("cache_read_input_tokens") or 0) or int(
|
|
599
|
+
totals.get("cache_creation_input_tokens") or 0
|
|
600
|
+
):
|
|
601
|
+
console.print(
|
|
602
|
+
"[dim]Cache read/write tokens can reduce provider cost or latency, but they still "
|
|
603
|
+
"occupy the current request context window.[/dim]"
|
|
604
|
+
)
|
|
605
|
+
corrected_total = int(totals.get("corrected_usage_calls") or 0)
|
|
606
|
+
if corrected_total > 0:
|
|
607
|
+
console.print(
|
|
608
|
+
"[yellow]Usage corrected:[/yellow] "
|
|
609
|
+
f"{corrected_total} provider usage record(s) contained inconsistent or character-like "
|
|
610
|
+
"token fields and were corrected before display."
|
|
611
|
+
)
|
|
612
|
+
request_estimate = totals.get("request_token_estimate")
|
|
613
|
+
if isinstance(request_estimate, dict):
|
|
614
|
+
schema_over_calls = int(request_estimate.get("tool_schema_budget_exceeded_calls") or 0)
|
|
615
|
+
if schema_over_calls > 0:
|
|
616
|
+
schema_overage = int(request_estimate.get("tool_schema_budget_overage_tokens") or 0)
|
|
617
|
+
largest_tool = int(request_estimate.get("tool_schema_largest_tool_tokens") or 0)
|
|
618
|
+
console.print(
|
|
619
|
+
"[yellow]Tool schema shadow budget:[/yellow] "
|
|
620
|
+
f"{schema_over_calls} call(s) exceeded the schema budget by "
|
|
621
|
+
f"{_format_exact_token_count(schema_overage)} total tokens "
|
|
622
|
+
f"(largest single tool {_format_exact_token_count(largest_tool)})."
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _print_chat_context(*, console: Console, session: Any) -> None:
|
|
627
|
+
from ..chat import _print_chat_context_impl
|
|
628
|
+
|
|
629
|
+
return _print_chat_context_impl(_cli_module_for_legacy_impl(), console=console, session=session)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _chat_used_models(session: Any) -> list[str]:
|
|
633
|
+
models: list[str] = []
|
|
634
|
+
seen: set[str] = set()
|
|
635
|
+
|
|
636
|
+
def _add(raw_value: Any) -> None:
|
|
637
|
+
value = str(raw_value or "").strip()
|
|
638
|
+
if not value:
|
|
639
|
+
return
|
|
640
|
+
key = value.casefold()
|
|
641
|
+
if key in seen:
|
|
642
|
+
return
|
|
643
|
+
seen.add(key)
|
|
644
|
+
models.append(value)
|
|
645
|
+
|
|
646
|
+
_add(getattr(getattr(session, "client", None), "model", ""))
|
|
647
|
+
_add(getattr(getattr(session, "cfg", None), "model", ""))
|
|
648
|
+
|
|
649
|
+
summary = getattr(session, "usage_summary", None)
|
|
650
|
+
rows_fn = getattr(summary, "by_model_rows", None)
|
|
651
|
+
if callable(rows_fn):
|
|
652
|
+
try:
|
|
653
|
+
rows = rows_fn()
|
|
654
|
+
except Exception: # noqa: BLE001
|
|
655
|
+
rows = []
|
|
656
|
+
if isinstance(rows, list):
|
|
657
|
+
for row in rows:
|
|
658
|
+
if not isinstance(row, dict):
|
|
659
|
+
continue
|
|
660
|
+
_add(row.get("model"))
|
|
661
|
+
|
|
662
|
+
return models
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _chat_config_usage_lines() -> list[str]:
|
|
666
|
+
return [
|
|
667
|
+
"/config",
|
|
668
|
+
(
|
|
669
|
+
"/config set <model|index> <context_window_tokens> <max_output_tokens> "
|
|
670
|
+
"[supports_vision] [input_cost_per_token] [output_cost_per_token]"
|
|
671
|
+
),
|
|
672
|
+
"/config clear <model|index>",
|
|
673
|
+
"Example: /config set 1 128000 4096 false",
|
|
674
|
+
]
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _print_chat_config_usage(*, console: Console) -> None:
|
|
678
|
+
console.print("[yellow]Usage:[/yellow]")
|
|
679
|
+
for line in _chat_config_usage_lines():
|
|
680
|
+
console.print(f" {line}")
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _resolve_chat_model_ref(*, session: Any, raw_model_ref: str) -> tuple[str | None, str | None]:
|
|
684
|
+
model_ref = raw_model_ref.strip()
|
|
685
|
+
if not model_ref:
|
|
686
|
+
return None, "Missing model reference."
|
|
687
|
+
|
|
688
|
+
if model_ref.isdigit():
|
|
689
|
+
models = _chat_used_models(session)
|
|
690
|
+
if not models:
|
|
691
|
+
return None, "No tracked models yet. Set a model first with /model <name>."
|
|
692
|
+
index = int(model_ref)
|
|
693
|
+
if index < 1 or index > len(models):
|
|
694
|
+
return None, f"Invalid model index: {index}. Run /config to list tracked models."
|
|
695
|
+
return models[index - 1], None
|
|
696
|
+
|
|
697
|
+
return model_ref, None
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _merge_model_metadata_override(
|
|
701
|
+
*,
|
|
702
|
+
cfg: AppConfig,
|
|
703
|
+
model_name: str,
|
|
704
|
+
fields: dict[str, Any],
|
|
705
|
+
) -> dict[str, Any]:
|
|
706
|
+
raw_overrides = cfg.extra_fields.get("model_metadata_overrides")
|
|
707
|
+
overrides = dict(raw_overrides) if isinstance(raw_overrides, dict) else {}
|
|
708
|
+
raw_models = overrides.get("models")
|
|
709
|
+
models = dict(raw_models) if isinstance(raw_models, dict) else {}
|
|
710
|
+
|
|
711
|
+
raw_existing = models.get(model_name)
|
|
712
|
+
merged = dict(raw_existing) if isinstance(raw_existing, dict) else {}
|
|
713
|
+
merged.update(fields)
|
|
714
|
+
models[model_name] = merged
|
|
715
|
+
overrides["models"] = models
|
|
716
|
+
cfg.extra_fields["model_metadata_overrides"] = overrides
|
|
717
|
+
return merged
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _clear_model_metadata_override(*, cfg: AppConfig, model_name: str) -> bool:
|
|
721
|
+
raw_overrides = cfg.extra_fields.get("model_metadata_overrides")
|
|
722
|
+
if not isinstance(raw_overrides, dict):
|
|
723
|
+
return False
|
|
724
|
+
raw_models = raw_overrides.get("models")
|
|
725
|
+
if not isinstance(raw_models, dict):
|
|
726
|
+
return False
|
|
727
|
+
if model_name not in raw_models:
|
|
728
|
+
return False
|
|
729
|
+
|
|
730
|
+
models = dict(raw_models)
|
|
731
|
+
models.pop(model_name, None)
|
|
732
|
+
|
|
733
|
+
overrides = dict(raw_overrides)
|
|
734
|
+
if models:
|
|
735
|
+
overrides["models"] = models
|
|
736
|
+
else:
|
|
737
|
+
overrides.pop("models", None)
|
|
738
|
+
|
|
739
|
+
if overrides:
|
|
740
|
+
cfg.extra_fields["model_metadata_overrides"] = overrides
|
|
741
|
+
else:
|
|
742
|
+
cfg.extra_fields.pop("model_metadata_overrides", None)
|
|
743
|
+
return True
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _save_chat_model_metadata_override(
|
|
747
|
+
*,
|
|
748
|
+
session: Any,
|
|
749
|
+
model_name: str,
|
|
750
|
+
fields: dict[str, Any],
|
|
751
|
+
) -> tuple[Path, dict[str, Any]]:
|
|
752
|
+
cfg = load_config()
|
|
753
|
+
merged = _merge_model_metadata_override(cfg=cfg, model_name=model_name, fields=fields)
|
|
754
|
+
save_config(cfg)
|
|
755
|
+
|
|
756
|
+
runtime_cfg = getattr(session, "cfg", None)
|
|
757
|
+
if isinstance(runtime_cfg, AppConfig):
|
|
758
|
+
_merge_model_metadata_override(cfg=runtime_cfg, model_name=model_name, fields=fields)
|
|
759
|
+
return config_path(), merged
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def _clear_chat_model_metadata_override(*, session: Any, model_name: str) -> tuple[Path, bool]:
|
|
763
|
+
cfg = load_config()
|
|
764
|
+
cleared = _clear_model_metadata_override(cfg=cfg, model_name=model_name)
|
|
765
|
+
if cleared:
|
|
766
|
+
save_config(cfg)
|
|
767
|
+
|
|
768
|
+
runtime_cfg = getattr(session, "cfg", None)
|
|
769
|
+
if isinstance(runtime_cfg, AppConfig):
|
|
770
|
+
_clear_model_metadata_override(cfg=runtime_cfg, model_name=model_name)
|
|
771
|
+
return config_path(), cleared
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _chat_config_panel(*, session: Any) -> Panel:
|
|
775
|
+
models = _chat_used_models(session)
|
|
776
|
+
registry = getattr(session, "model_registry", None)
|
|
777
|
+
title = f"Model Config ({config_path()})"
|
|
778
|
+
|
|
779
|
+
if _patchable("_is_narrow_terminal", _is_narrow_terminal)():
|
|
780
|
+
lines: list[str] = []
|
|
781
|
+
if models:
|
|
782
|
+
lines.append("Tracked models:")
|
|
783
|
+
for idx, model_name in enumerate(models, start=1):
|
|
784
|
+
summary = ""
|
|
785
|
+
if registry is not None:
|
|
786
|
+
try:
|
|
787
|
+
meta = registry.get(model_name)
|
|
788
|
+
except Exception as e: # noqa: BLE001
|
|
789
|
+
summary = f"error={e}"
|
|
790
|
+
else:
|
|
791
|
+
field_sources = getattr(meta, "field_sources", {}) or {}
|
|
792
|
+
ctx_source = field_sources.get(
|
|
793
|
+
"context_window_tokens",
|
|
794
|
+
getattr(meta, "source", "unknown"),
|
|
795
|
+
)
|
|
796
|
+
summary = (
|
|
797
|
+
f"ctx={meta.context_window_tokens}, out={meta.max_output_tokens}, "
|
|
798
|
+
f"src={ctx_source}"
|
|
799
|
+
)
|
|
800
|
+
if summary:
|
|
801
|
+
lines.append(f"{idx}) {model_name} - {summary}")
|
|
802
|
+
else:
|
|
803
|
+
lines.append(f"{idx}) {model_name}")
|
|
804
|
+
else:
|
|
805
|
+
lines.append("No tracked models in this session yet.")
|
|
806
|
+
lines.append("")
|
|
807
|
+
lines.extend(_chat_config_usage_lines())
|
|
808
|
+
return _Panel("\n".join(lines), title=title, border_style="bright_black")
|
|
809
|
+
|
|
810
|
+
table = _Table(show_header=True, box=None, expand=True, padding=(0, 1), collapse_padding=True)
|
|
811
|
+
table.add_column("#", style=STYLE_EMPHASIS, no_wrap=True, width=3)
|
|
812
|
+
table.add_column("model", style=STYLE_CONTENT, no_wrap=False, ratio=4, overflow="fold")
|
|
813
|
+
table.add_column("ctx", style=STYLE_CONTENT, no_wrap=True, ratio=2)
|
|
814
|
+
table.add_column("max_out", style=STYLE_CONTENT, no_wrap=True, ratio=2)
|
|
815
|
+
table.add_column("source", style=STYLE_CONTENT, no_wrap=False, ratio=2)
|
|
816
|
+
table.add_column("note", style=STYLE_CONTENT, no_wrap=False, ratio=3, overflow="fold")
|
|
817
|
+
|
|
818
|
+
if models:
|
|
819
|
+
for idx, model_name in enumerate(models, start=1):
|
|
820
|
+
ctx_value = "-"
|
|
821
|
+
max_out_value = "-"
|
|
822
|
+
source = "-"
|
|
823
|
+
note = "-"
|
|
824
|
+
if registry is not None:
|
|
825
|
+
try:
|
|
826
|
+
meta = registry.get(model_name)
|
|
827
|
+
except Exception as e: # noqa: BLE001
|
|
828
|
+
note = str(e)
|
|
829
|
+
else:
|
|
830
|
+
field_sources = getattr(meta, "field_sources", {}) or {}
|
|
831
|
+
ctx_value = str(getattr(meta, "context_window_tokens", "-"))
|
|
832
|
+
max_out_value = str(getattr(meta, "max_output_tokens", "-"))
|
|
833
|
+
source = str(
|
|
834
|
+
field_sources.get(
|
|
835
|
+
"context_window_tokens",
|
|
836
|
+
getattr(meta, "source", "unknown"),
|
|
837
|
+
)
|
|
838
|
+
)
|
|
839
|
+
warnings = list(getattr(meta, "warnings", ())[:1])
|
|
840
|
+
note = warnings[0] if warnings else "-"
|
|
841
|
+
table.add_row(str(idx), model_name, ctx_value, max_out_value, source, note)
|
|
842
|
+
else:
|
|
843
|
+
table.add_row("-", "(no tracked models yet)", "-", "-", "-", "-")
|
|
844
|
+
|
|
845
|
+
usage_text = "\n".join(_chat_config_usage_lines())
|
|
846
|
+
content = _table_grid(expand=True)
|
|
847
|
+
content.add_row(table)
|
|
848
|
+
content.add_row("")
|
|
849
|
+
content.add_row(usage_text)
|
|
850
|
+
return _Panel(content, title=title, border_style="bright_black")
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _chat_turn_usage_line(session: Any) -> tuple[str | None, str | None] | None:
|
|
854
|
+
ctx_percent = _chat_context_percent_value(session)
|
|
855
|
+
ctx_display = _format_chat_context_percent(ctx_percent)
|
|
856
|
+
ctx_segment = f"[dim]context left:[/dim] {ctx_display}" if ctx_display != "n/a" else None
|
|
857
|
+
warning_line = _chat_usage_warning_line(_chat_effective_budget_percent_value(session))
|
|
858
|
+
if not _chat_usage_hud_enabled(session):
|
|
859
|
+
return (ctx_segment, warning_line) if ctx_segment or warning_line else None
|
|
860
|
+
|
|
861
|
+
summary = getattr(session, "usage_summary", None)
|
|
862
|
+
if summary is None:
|
|
863
|
+
return (ctx_segment, warning_line) if ctx_segment or warning_line else None
|
|
864
|
+
totals = summary.totals()
|
|
865
|
+
total_tokens = int(totals.get("total_tokens") or 0)
|
|
866
|
+
if total_tokens <= 0:
|
|
867
|
+
return (ctx_segment, warning_line) if ctx_segment or warning_line else None
|
|
868
|
+
|
|
869
|
+
segments = [
|
|
870
|
+
f"{_format_compact_token_count(total_tokens)} [dim]tokens[/dim]",
|
|
871
|
+
f"[dim]↓[/dim] {_format_compact_token_count(totals.get('prompt_tokens'))}",
|
|
872
|
+
f"[dim]↑[/dim] {_format_compact_token_count(totals.get('completion_tokens'))}",
|
|
873
|
+
]
|
|
874
|
+
cache_read_tokens = int(totals.get("cache_read_input_tokens") or 0)
|
|
875
|
+
cache_write_tokens = int(totals.get("cache_creation_input_tokens") or 0)
|
|
876
|
+
if cache_read_tokens or cache_write_tokens:
|
|
877
|
+
segments.append(
|
|
878
|
+
f"[dim]cache[/dim] r:{_format_compact_token_count(cache_read_tokens)} "
|
|
879
|
+
f"w:{_format_compact_token_count(cache_write_tokens)}"
|
|
880
|
+
)
|
|
881
|
+
if ctx_segment is not None:
|
|
882
|
+
segments.append(ctx_segment)
|
|
883
|
+
return (" ".join(segments), warning_line)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _chat_bottom_toolbar(
|
|
887
|
+
*,
|
|
888
|
+
session: Any,
|
|
889
|
+
pending_images: list[str],
|
|
890
|
+
forge_state: _ForgeChatState | None = None,
|
|
891
|
+
plan_mode_enabled: bool = False,
|
|
892
|
+
) -> str:
|
|
893
|
+
model = str(getattr(getattr(session, "client", None), "model", "?"))
|
|
894
|
+
mode = str(getattr(session, "mode", "?"))
|
|
895
|
+
stream_enabled = bool(getattr(session, "stream", False))
|
|
896
|
+
stream = "on" if stream_enabled else "off"
|
|
897
|
+
temperature_raw = getattr(getattr(session, "client", None), "temperature", "?")
|
|
898
|
+
temperature = str(temperature_raw)
|
|
899
|
+
trace = _patchable("_chat_trace_level", _chat_trace_level)(session)
|
|
900
|
+
ctx_hud = _chat_context_hud_value(session)
|
|
901
|
+
subagents = "on" if bool(getattr(session, "subagents_enabled", False)) else "off"
|
|
902
|
+
cfg = getattr(session, "cfg", None)
|
|
903
|
+
toolbar_items_raw = getattr(cfg, "toolbar_items", None)
|
|
904
|
+
if toolbar_items_raw is None:
|
|
905
|
+
visible_items: set[str] = set(_DEFAULT_TOOLBAR_ITEMS)
|
|
906
|
+
else:
|
|
907
|
+
visible_items = {
|
|
908
|
+
str(item).strip().lower() for item in toolbar_items_raw if str(item).strip()
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
tokens_part: str | None = None
|
|
912
|
+
cost_part: str | None = None
|
|
913
|
+
if _chat_usage_hud_enabled(session):
|
|
914
|
+
summary = getattr(session, "usage_summary", None)
|
|
915
|
+
if summary is not None:
|
|
916
|
+
totals = summary.totals()
|
|
917
|
+
total_tokens = int(totals.get("total_tokens") or 0)
|
|
918
|
+
known_cost = _known_cost_value(totals)
|
|
919
|
+
unknown_calls = int(totals.get("unknown_cost_calls") or 0)
|
|
920
|
+
cost_hud = _format_cost_with_unknown(
|
|
921
|
+
known_cost=known_cost,
|
|
922
|
+
unknown_calls=unknown_calls,
|
|
923
|
+
style="hud",
|
|
924
|
+
)
|
|
925
|
+
tokens_part = f"tokens={total_tokens}"
|
|
926
|
+
cost_part = f"cost={cost_hud}"
|
|
927
|
+
|
|
928
|
+
default_temperature_raw = getattr(session, "_toolbar_default_temperature", None)
|
|
929
|
+
if default_temperature_raw is None:
|
|
930
|
+
default_temperature_raw = getattr(getattr(session, "cfg", None), "chat_temperature", None)
|
|
931
|
+
try:
|
|
932
|
+
current_temperature = float(temperature_raw)
|
|
933
|
+
except (TypeError, ValueError):
|
|
934
|
+
current_temperature = None
|
|
935
|
+
try:
|
|
936
|
+
default_temperature = float(default_temperature_raw)
|
|
937
|
+
except (TypeError, ValueError):
|
|
938
|
+
default_temperature = None
|
|
939
|
+
show_temperature = False
|
|
940
|
+
if current_temperature is None:
|
|
941
|
+
show_temperature = temperature.strip() not in {"", "?"}
|
|
942
|
+
elif default_temperature is None:
|
|
943
|
+
show_temperature = True
|
|
944
|
+
else:
|
|
945
|
+
show_temperature = abs(current_temperature - default_temperature) > 1e-9
|
|
946
|
+
|
|
947
|
+
toolbar_parts: list[str] = []
|
|
948
|
+
if "mode" in visible_items:
|
|
949
|
+
toolbar_parts.append(mode)
|
|
950
|
+
if "model" in visible_items:
|
|
951
|
+
toolbar_parts.append(model)
|
|
952
|
+
if "stream" in visible_items and stream == "off":
|
|
953
|
+
toolbar_parts.append("no-stream")
|
|
954
|
+
if "trace" in visible_items and trace != "compact":
|
|
955
|
+
toolbar_parts.append(f"trace {trace}")
|
|
956
|
+
if "images" in visible_items and pending_images:
|
|
957
|
+
count = len(pending_images)
|
|
958
|
+
toolbar_parts.append(f"{count} image" + ("" if count == 1 else "s"))
|
|
959
|
+
if "temp" in visible_items and show_temperature:
|
|
960
|
+
toolbar_parts.append(f"temp {temperature}")
|
|
961
|
+
if "ctx" in visible_items:
|
|
962
|
+
toolbar_parts.append(f"context left: {ctx_hud}")
|
|
963
|
+
if "subagents" in visible_items:
|
|
964
|
+
toolbar_parts.append(f"subagents {subagents}")
|
|
965
|
+
if "tokens" in visible_items and tokens_part:
|
|
966
|
+
token_value = tokens_part.split("=", 1)[-1]
|
|
967
|
+
toolbar_parts.append(f"{token_value} tok")
|
|
968
|
+
if "cost" in visible_items and cost_part:
|
|
969
|
+
cost_value = cost_part.split("=", 1)[-1]
|
|
970
|
+
toolbar_parts.append(cost_value)
|
|
971
|
+
|
|
972
|
+
if (
|
|
973
|
+
forge_state is not None
|
|
974
|
+
and _is_forge_ui_mode(forge_state.ui_mode)
|
|
975
|
+
and "forge" in visible_items
|
|
976
|
+
):
|
|
977
|
+
run_id = forge_state.paths.run_id if forge_state.paths is not None else "-"
|
|
978
|
+
task_count = len((forge_state.plan or {}).get("tasks") or [])
|
|
979
|
+
toolbar_parts.append(run_id)
|
|
980
|
+
toolbar_parts.append(f"{task_count} task" + ("" if task_count == 1 else "s"))
|
|
981
|
+
elif forge_state is not None and "plan" in visible_items:
|
|
982
|
+
if plan_mode_enabled:
|
|
983
|
+
toolbar_parts.append("plan readonly")
|
|
984
|
+
else:
|
|
985
|
+
toolbar_parts.append("plan /plan <task>")
|
|
986
|
+
if plan_mode_enabled and not (
|
|
987
|
+
forge_state is not None and _is_forge_ui_mode(forge_state.ui_mode)
|
|
988
|
+
):
|
|
989
|
+
toolbar_parts.append("Esc /plan off")
|
|
990
|
+
if not toolbar_parts:
|
|
991
|
+
return " /help "
|
|
992
|
+
return " " + " · ".join([*toolbar_parts, "/help"]) + " "
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def _chat_prompt_label(*, ui_mode: str = "chat", mode: str = "") -> str:
|
|
996
|
+
if _is_forge_ui_mode(ui_mode):
|
|
997
|
+
# ASCII-safe fallback (used when prompt_toolkit HTML is unavailable);
|
|
998
|
+
# the interactive path uses the ``\u25b8`` form below.
|
|
999
|
+
return "forge > "
|
|
1000
|
+
_ = mode
|
|
1001
|
+
return _CHAT_PROMPT_TEXT
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def _chat_prompt_label_formatted(*, ui_mode: str = "chat", mode: str = "") -> Any:
|
|
1005
|
+
try:
|
|
1006
|
+
from prompt_toolkit.formatted_text import HTML
|
|
1007
|
+
except Exception:
|
|
1008
|
+
return _chat_prompt_label(ui_mode=ui_mode, mode=mode)
|
|
1009
|
+
if _is_forge_ui_mode(ui_mode):
|
|
1010
|
+
return HTML("<b>forge</b> <ansicyan>\u25b8</ansicyan> ")
|
|
1011
|
+
_ = mode
|
|
1012
|
+
return HTML("<b>></b> ")
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def _chat_prompt_fallback_label(*, ui_mode: str = "chat", mode: str = "") -> str:
|
|
1016
|
+
if _is_forge_ui_mode(ui_mode):
|
|
1017
|
+
return "forge"
|
|
1018
|
+
_ = mode
|
|
1019
|
+
return _CHAT_PROMPT_FALLBACK_LABEL
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def _accept_chat_suggestion_or_complete(event: Any) -> None:
|
|
1023
|
+
current_buffer = getattr(event, "current_buffer", None)
|
|
1024
|
+
if current_buffer is None:
|
|
1025
|
+
return
|
|
1026
|
+
complete_state = getattr(current_buffer, "complete_state", None)
|
|
1027
|
+
if complete_state is not None:
|
|
1028
|
+
completion = getattr(complete_state, "current_completion", None)
|
|
1029
|
+
apply_completion = getattr(current_buffer, "apply_completion", None)
|
|
1030
|
+
if completion is not None and callable(apply_completion):
|
|
1031
|
+
apply_completion(completion)
|
|
1032
|
+
return
|
|
1033
|
+
complete_next = getattr(current_buffer, "complete_next", None)
|
|
1034
|
+
if callable(complete_next):
|
|
1035
|
+
complete_next()
|
|
1036
|
+
return
|
|
1037
|
+
suggestion = getattr(current_buffer, "suggestion", None)
|
|
1038
|
+
suggestion_text = str(getattr(suggestion, "text", "") or "")
|
|
1039
|
+
document = getattr(current_buffer, "document", None)
|
|
1040
|
+
if suggestion_text and bool(getattr(document, "is_cursor_at_the_end", False)):
|
|
1041
|
+
insert_text = getattr(current_buffer, "insert_text", None)
|
|
1042
|
+
if callable(insert_text):
|
|
1043
|
+
insert_text(suggestion_text)
|
|
1044
|
+
return
|
|
1045
|
+
start_completion = getattr(current_buffer, "start_completion", None)
|
|
1046
|
+
if callable(start_completion):
|
|
1047
|
+
start_completion(select_first=False)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def _submitted_prompt_total_lines(
|
|
1051
|
+
*,
|
|
1052
|
+
submitted_text: str,
|
|
1053
|
+
prompt_label: str,
|
|
1054
|
+
terminal_columns: int | None = None,
|
|
1055
|
+
) -> int:
|
|
1056
|
+
columns = int(terminal_columns or 0)
|
|
1057
|
+
if columns <= 0:
|
|
1058
|
+
columns = _terminal_width(default=80)
|
|
1059
|
+
if columns <= 0:
|
|
1060
|
+
columns = 80
|
|
1061
|
+
|
|
1062
|
+
normalized = str(submitted_text or "").replace("\r\n", "\n").replace("\r", "\n")
|
|
1063
|
+
segments = normalized.split("\n")
|
|
1064
|
+
first_prefix = max(1, visibleLength(prompt_label))
|
|
1065
|
+
total_lines = 0
|
|
1066
|
+
for idx, segment in enumerate(segments):
|
|
1067
|
+
segment_width = visibleLength(segment)
|
|
1068
|
+
prefix_width = first_prefix if idx == 0 else 0
|
|
1069
|
+
visual_width = segment_width + prefix_width
|
|
1070
|
+
if visual_width <= 0:
|
|
1071
|
+
total_lines += 1
|
|
1072
|
+
continue
|
|
1073
|
+
total_lines += ((visual_width - 1) // columns) + 1
|
|
1074
|
+
return max(1, total_lines)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _clear_previous_terminal_lines_ansi(line_count: int) -> str:
|
|
1078
|
+
count = max(1, int(line_count or 1))
|
|
1079
|
+
parts: list[str] = []
|
|
1080
|
+
for _ in range(count):
|
|
1081
|
+
parts.append("\x1b[1A\r\x1b[2K")
|
|
1082
|
+
parts.append("\r")
|
|
1083
|
+
return "".join(parts)
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _clear_submitted_prompt_line(
|
|
1087
|
+
*,
|
|
1088
|
+
submitted_text: str = "",
|
|
1089
|
+
prompt_label: str = _CHAT_PROMPT_TEXT,
|
|
1090
|
+
terminal_columns: int | None = None,
|
|
1091
|
+
) -> None:
|
|
1092
|
+
if not _patchable("_is_interactive_terminal", _is_interactive_terminal)():
|
|
1093
|
+
return
|
|
1094
|
+
term = (os.environ.get("TERM") or "").strip().lower()
|
|
1095
|
+
if term in {"", "dumb"}:
|
|
1096
|
+
return
|
|
1097
|
+
try:
|
|
1098
|
+
clear_count = _submitted_prompt_total_lines(
|
|
1099
|
+
submitted_text=submitted_text,
|
|
1100
|
+
prompt_label=prompt_label,
|
|
1101
|
+
terminal_columns=terminal_columns,
|
|
1102
|
+
)
|
|
1103
|
+
sys.stdout.write(_clear_previous_terminal_lines_ansi(clear_count))
|
|
1104
|
+
sys.stdout.flush()
|
|
1105
|
+
except Exception:
|
|
1106
|
+
pass
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def _redact_chat_error_text(text: str) -> str:
|
|
1110
|
+
out = str(text or "")
|
|
1111
|
+
for pattern in _CHAT_LLM_ERROR_REDACT_PATTERNS:
|
|
1112
|
+
if pattern.pattern.lower().startswith("(authorization"):
|
|
1113
|
+
out = pattern.sub(r"\1[REDACTED]", out)
|
|
1114
|
+
continue
|
|
1115
|
+
if pattern.pattern.lower().startswith("(bearer"):
|
|
1116
|
+
out = pattern.sub(r"\1[REDACTED]", out)
|
|
1117
|
+
continue
|
|
1118
|
+
out = pattern.sub("[REDACTED]", out)
|
|
1119
|
+
return out
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def _truncate_chat_error_text(text: str, *, max_chars: int = _CHAT_LLM_ERROR_MAX_CHARS) -> str:
|
|
1123
|
+
clean = _redact_chat_error_text(str(text).strip())
|
|
1124
|
+
if not clean:
|
|
1125
|
+
return "No additional error details."
|
|
1126
|
+
if len(clean) <= max_chars:
|
|
1127
|
+
return clean
|
|
1128
|
+
return clean[: max_chars - 15].rstrip() + "...(truncated)"
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _chat_llm_error_panel(*, message: str) -> Panel:
|
|
1132
|
+
friendly_message = friendly_llm_error_message(message)
|
|
1133
|
+
display = classify_llm_error_display(friendly_message)
|
|
1134
|
+
body_lines = [_truncate_chat_error_text(friendly_message)]
|
|
1135
|
+
if display.guidance_lines:
|
|
1136
|
+
body_lines.append("")
|
|
1137
|
+
body_lines.extend(display.guidance_lines)
|
|
1138
|
+
return _Panel("\n".join(body_lines), title=display.title, border_style="red")
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
def _humanized_chat_llm_error_message(error: Exception) -> str:
|
|
1142
|
+
return friendly_llm_error_message(error)
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _render_chat_llm_error(*, session: Any, console: Console, error: Exception) -> None:
|
|
1146
|
+
message = _humanized_chat_llm_error_message(error)
|
|
1147
|
+
surface = getattr(session, "surface", None)
|
|
1148
|
+
on_error = getattr(surface, "on_error", None)
|
|
1149
|
+
if callable(on_error):
|
|
1150
|
+
try:
|
|
1151
|
+
on_error(message)
|
|
1152
|
+
except Exception: # noqa: BLE001
|
|
1153
|
+
console.print("")
|
|
1154
|
+
console.print(_chat_llm_error_panel(message=message))
|
|
1155
|
+
return
|
|
1156
|
+
if bool(getattr(surface, "renders_error_panel", False)):
|
|
1157
|
+
return
|
|
1158
|
+
console.print("")
|
|
1159
|
+
console.print(_chat_llm_error_panel(message=message))
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def _plan_mode_action_rows() -> list[tuple[str, str, str]]:
|
|
1163
|
+
return [
|
|
1164
|
+
(
|
|
1165
|
+
"approve",
|
|
1166
|
+
"Approve and execute",
|
|
1167
|
+
"Run the task immediately using this approved draft.",
|
|
1168
|
+
),
|
|
1169
|
+
("propose", "Propose changes", "Provide feedback and regenerate a revised draft."),
|
|
1170
|
+
("discard", "Discard this plan", "Cancel this draft and return to chat."),
|
|
1171
|
+
]
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _plan_mode_actions_panel(
|
|
1175
|
+
*,
|
|
1176
|
+
selected_action: str | None = None,
|
|
1177
|
+
interactive: bool = False,
|
|
1178
|
+
) -> Any:
|
|
1179
|
+
from rich.console import Group
|
|
1180
|
+
|
|
1181
|
+
selected = (selected_action or "").strip().casefold()
|
|
1182
|
+
renderables: list[Any] = []
|
|
1183
|
+
for idx, (value, label, desc) in enumerate(_plan_mode_action_rows(), start=1):
|
|
1184
|
+
row_selected = str(value).strip().casefold() == selected
|
|
1185
|
+
renderables.extend(
|
|
1186
|
+
_plan_mode_picker_row_renderables(
|
|
1187
|
+
label=f"{idx}) {label}",
|
|
1188
|
+
desc=desc,
|
|
1189
|
+
selected=row_selected,
|
|
1190
|
+
)
|
|
1191
|
+
)
|
|
1192
|
+
if interactive:
|
|
1193
|
+
renderables.append(_plan_mode_picker_hint_renderable())
|
|
1194
|
+
return Group(*renderables)
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def _select_plan_mode_action_interactive(*, console: Console) -> tuple[str | None, bool]:
|
|
1198
|
+
rows = _plan_mode_action_rows()
|
|
1199
|
+
return _patchable("_run_inline_option_selector", _run_inline_option_selector)(
|
|
1200
|
+
console=console,
|
|
1201
|
+
rows=rows,
|
|
1202
|
+
current_value="approve",
|
|
1203
|
+
panel_builder=lambda selected, interactive: _plan_mode_actions_panel(
|
|
1204
|
+
selected_action=selected,
|
|
1205
|
+
interactive=interactive,
|
|
1206
|
+
),
|
|
1207
|
+
unavailable_label="Plan action picker",
|
|
1208
|
+
use_alt_screen=False,
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def _prompt_plan_mode_action(*, console: Console) -> str | None:
|
|
1213
|
+
selected_action, picker_available = _patchable(
|
|
1214
|
+
"_select_plan_mode_action_interactive",
|
|
1215
|
+
_select_plan_mode_action_interactive,
|
|
1216
|
+
)(console=console)
|
|
1217
|
+
if picker_available:
|
|
1218
|
+
return selected_action
|
|
1219
|
+
|
|
1220
|
+
console.print(_plan_mode_actions_panel())
|
|
1221
|
+
try:
|
|
1222
|
+
choice = _patchable("_prompt_ask", _prompt_ask)(
|
|
1223
|
+
"Select option", choices=["1", "2", "3"], console=console
|
|
1224
|
+
)
|
|
1225
|
+
except (EOFError, KeyboardInterrupt):
|
|
1226
|
+
console.print("")
|
|
1227
|
+
return None
|
|
1228
|
+
if choice == "1":
|
|
1229
|
+
return "approve"
|
|
1230
|
+
if choice == "2":
|
|
1231
|
+
return "propose"
|
|
1232
|
+
if choice == "3":
|
|
1233
|
+
return "discard"
|
|
1234
|
+
return None
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _prompt_plan_mode_feedback(*, console: Console) -> str | None:
|
|
1238
|
+
try:
|
|
1239
|
+
return _patchable("_prompt_text_with_escape", _prompt_text_with_escape)(
|
|
1240
|
+
"Plan feedback",
|
|
1241
|
+
escape_hint="Esc to cancel draft",
|
|
1242
|
+
)
|
|
1243
|
+
except (EOFError, KeyboardInterrupt):
|
|
1244
|
+
console.print("")
|
|
1245
|
+
return None
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
__all__ = [name for name in globals() if (not name.startswith("__") or name == "__version__")]
|