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,796 @@
|
|
|
1
|
+
"""``TuiSurface`` — renders agent events into the full-screen TUI transcript.
|
|
2
|
+
|
|
3
|
+
The agent runtime drives surfaces through the legacy ``on_*`` callbacks (same
|
|
4
|
+
path ``RichSurface`` uses), calling them synchronously from whatever thread runs
|
|
5
|
+
``session.run_turn``. In the TUI that is a worker thread, so every handler just
|
|
6
|
+
mutates the thread-safe :class:`TuiTranscript` (which schedules a redraw); none
|
|
7
|
+
of them touch prompt_toolkit widgets directly.
|
|
8
|
+
|
|
9
|
+
Message/tool ``emit_*`` events are additive duplicates of the ``on_*`` calls and
|
|
10
|
+
are intentionally left undefined (defining no-op versions would double-render).
|
|
11
|
+
Error/warning/info are EITHER/OR in the runtime, so we provide real
|
|
12
|
+
``emit_error``/``emit_warning``/``emit_info`` handlers -- otherwise the
|
|
13
|
+
runtime's capability probe would drop them. Info events are persistent
|
|
14
|
+
transcript entries; progress updates remain ephemeral status text.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import threading
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
from ...llm_error_display import friendly_llm_error_message
|
|
23
|
+
from ...subagent_labels import subagent_identity
|
|
24
|
+
from ...surface.types import (
|
|
25
|
+
ApprovalDecision,
|
|
26
|
+
ApprovalRequest,
|
|
27
|
+
PatchEvent,
|
|
28
|
+
StatusEvent,
|
|
29
|
+
SubagentEndEvent,
|
|
30
|
+
SubagentStartEvent,
|
|
31
|
+
ToolEndEvent,
|
|
32
|
+
ToolOutputEvent,
|
|
33
|
+
ToolStartEvent,
|
|
34
|
+
)
|
|
35
|
+
from .forge_status import FORGE_RUNNING_STATES, forge_status_counts
|
|
36
|
+
from .transcript import TuiTranscript
|
|
37
|
+
|
|
38
|
+
# Per-worker-thread cancellation token. The TUI sets this at the top of each turn
|
|
39
|
+
# worker; once the user soft-interrupts, that worker's token flips cancelled and
|
|
40
|
+
# every output handler below drops its events — so an abandoned turn (still blocked
|
|
41
|
+
# waiting on a slow model) can never paint into the transcript after the interrupt.
|
|
42
|
+
# It is thread-local, so a *new* turn on a fresh worker is unaffected.
|
|
43
|
+
_worker_ctx = threading.local()
|
|
44
|
+
_TRACE_LEVELS = frozenset({"off", "compact", "full"})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _normalize_trace_level(value: object, *, fallback: str = "compact") -> str:
|
|
48
|
+
normalized = str(value or "").strip().lower()
|
|
49
|
+
return normalized if normalized in _TRACE_LEVELS else fallback
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def set_active_cancellation(token: object | None) -> None:
|
|
53
|
+
_worker_ctx.token = token
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _worker_cancelled() -> bool:
|
|
57
|
+
token = getattr(_worker_ctx, "token", None)
|
|
58
|
+
return bool(token is not None and getattr(token, "is_cancelled", False))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _tool_label(name: str) -> str:
|
|
62
|
+
try:
|
|
63
|
+
from ...tools.registry import tool_display_name
|
|
64
|
+
|
|
65
|
+
return tool_display_name(name)
|
|
66
|
+
except Exception:
|
|
67
|
+
return name
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _tool_detail(name: str, args: object) -> str:
|
|
71
|
+
"""Short argument preview (search query, fetched URL, file path) so repeated
|
|
72
|
+
lines of the same tool stay distinguishable in the transcript. Shell-category
|
|
73
|
+
tools are excluded: full command lines can carry secrets and are not shown at
|
|
74
|
+
the default trace level."""
|
|
75
|
+
try:
|
|
76
|
+
from ...tools.registry import get_builtin_tool_metadata, tool_input_preview
|
|
77
|
+
|
|
78
|
+
spec = get_builtin_tool_metadata(name)
|
|
79
|
+
if spec is not None and "shell" in spec.categories:
|
|
80
|
+
return ""
|
|
81
|
+
detail = tool_input_preview(name, args if isinstance(args, dict) else {})
|
|
82
|
+
except Exception:
|
|
83
|
+
return ""
|
|
84
|
+
clean = " ".join(str(detail or "").split())
|
|
85
|
+
if clean in {"", "-"}:
|
|
86
|
+
return ""
|
|
87
|
+
if len(clean) > 64:
|
|
88
|
+
clean = clean[:63] + "…"
|
|
89
|
+
return clean
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _format_ms(elapsed_ms: int) -> str:
|
|
93
|
+
try:
|
|
94
|
+
elapsed_ms = int(elapsed_ms)
|
|
95
|
+
except (TypeError, ValueError):
|
|
96
|
+
return ""
|
|
97
|
+
if elapsed_ms < 1000:
|
|
98
|
+
return f"{elapsed_ms}ms"
|
|
99
|
+
return f"{elapsed_ms / 1000.0:.1f}s"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _truncate(text: str, *, limit: int = 160) -> str:
|
|
103
|
+
clean = " ".join(str(text).split())
|
|
104
|
+
if len(clean) <= limit:
|
|
105
|
+
return clean
|
|
106
|
+
return clean[: limit - 1] + "…"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TuiSurface:
|
|
110
|
+
"""Surface implementation backed by a :class:`TuiTranscript`."""
|
|
111
|
+
|
|
112
|
+
renders_error_panel = True
|
|
113
|
+
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
transcript: TuiTranscript,
|
|
117
|
+
*,
|
|
118
|
+
request_approval_ui: Callable[[ApprovalRequest], ApprovalDecision] | None = None,
|
|
119
|
+
on_hud_refresh: Callable[[], None] | None = None,
|
|
120
|
+
on_subagent_activity: Callable[[str | None], None] | None = None,
|
|
121
|
+
on_subagent_activities: Callable[[tuple[str, ...]], None] | None = None,
|
|
122
|
+
on_subagent_run_started: Callable[[SubagentStartEvent], None] | None = None,
|
|
123
|
+
) -> None:
|
|
124
|
+
self._t = transcript
|
|
125
|
+
self._approval_ui = request_approval_ui
|
|
126
|
+
self._trace_level = "compact"
|
|
127
|
+
# Argument previews captured at tool start, keyed by call id, so the
|
|
128
|
+
# committed "✓ …" line can show what the tool actually worked on.
|
|
129
|
+
self._tool_details: dict[str, str] = {}
|
|
130
|
+
# Name of the tool whose "✓/↳" line was committed last, so consecutive
|
|
131
|
+
# runs of the same tool group as continuation rows instead of repeating
|
|
132
|
+
# the tool name (e.g. four "Search Web" lines).
|
|
133
|
+
self._last_tool_trace_name: str | None = None
|
|
134
|
+
# Live footer HUD (context/tokens/cost) refresh. The agent runtime drives
|
|
135
|
+
# the surface from the SAME worker thread that runs ``run_turn``, so this
|
|
136
|
+
# callback reads session state at quiescent points (tool-end / message-done)
|
|
137
|
+
# with no concurrent mutation. Throttled so a tool-heavy turn does not
|
|
138
|
+
# re-estimate the whole transcript on every step. Without it the footer would
|
|
139
|
+
# only update once the entire (possibly long, multi-step) turn completes,
|
|
140
|
+
# leaving the bottom-right numbers frozen while the agent is visibly working.
|
|
141
|
+
self._hud_refresh = on_hud_refresh
|
|
142
|
+
self._hud_last_refresh: float = 0.0
|
|
143
|
+
# Live Forge execution context (set for the duration of /execute plan): the
|
|
144
|
+
# run paths (to refresh task statuses from plan.json), a debounce stamp, the
|
|
145
|
+
# worker cancellation token, and the run id (to drop stale/superseded events).
|
|
146
|
+
self._forge_paths: object | None = None
|
|
147
|
+
self._forge_last_refresh: float = 0.0
|
|
148
|
+
self._forge_token: object | None = None
|
|
149
|
+
self._forge_run_id: str = ""
|
|
150
|
+
# Live subagent identity. The stack tracks concurrent subagent runs
|
|
151
|
+
# (start pushes, end pops); the callback tells the app which name to pin
|
|
152
|
+
# in the footer badge — None clears it. Entries are keyed by the pushing
|
|
153
|
+
# THREAD as well as the name: a run's start and end always fire on the
|
|
154
|
+
# same thread (the tool's run() is synchronous), while a stale end from
|
|
155
|
+
# an abandoned turn arrives on a thread that no longer owns an entry —
|
|
156
|
+
# so it can never pop a same-named agent the next turn just started.
|
|
157
|
+
# Cleared at every turn boundary and on soft-interrupt so an interrupted
|
|
158
|
+
# run can never leave a stale badge.
|
|
159
|
+
self._on_subagent_activity = on_subagent_activity
|
|
160
|
+
self._on_subagent_activities = on_subagent_activities
|
|
161
|
+
self._on_subagent_run_started = on_subagent_run_started
|
|
162
|
+
self._subagent_stack: list[tuple[int, str, str, str]] = []
|
|
163
|
+
self._subagent_activity: dict[str, str] = {}
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def trace_level(self) -> str:
|
|
167
|
+
return self._trace_level
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def reasoning_trace_enabled(self) -> bool:
|
|
171
|
+
"""Safe provider summaries are requested only while trace is visible."""
|
|
172
|
+
|
|
173
|
+
return self._trace_level != "off"
|
|
174
|
+
|
|
175
|
+
def set_trace_level(self, level: str) -> str:
|
|
176
|
+
self._trace_level = _normalize_trace_level(level, fallback=self._trace_level)
|
|
177
|
+
self._t.set_trace_level(self._trace_level)
|
|
178
|
+
if self._trace_level == "off":
|
|
179
|
+
# A level change is display-only: close already-visible transient
|
|
180
|
+
# reasoning/status without touching the model request or transcript
|
|
181
|
+
# accounting state.
|
|
182
|
+
self._t.end_reasoning()
|
|
183
|
+
self._t.set_status(None)
|
|
184
|
+
return self._trace_level
|
|
185
|
+
|
|
186
|
+
def _maybe_refresh_hud(self) -> None:
|
|
187
|
+
"""Refresh the footer HUD mid-turn (throttled), so context/tokens/cost tick
|
|
188
|
+
up while a multi-step turn runs instead of only at the end. Runs on the
|
|
189
|
+
worker thread; the callback (the caller's end-of-turn refresher) mutates the
|
|
190
|
+
shared ``TuiState`` and the next transcript redraw repaints the footer."""
|
|
191
|
+
fn = self._hud_refresh
|
|
192
|
+
if fn is None or _worker_cancelled():
|
|
193
|
+
return
|
|
194
|
+
import time
|
|
195
|
+
|
|
196
|
+
now = time.monotonic()
|
|
197
|
+
if now - self._hud_last_refresh < 0.4:
|
|
198
|
+
return
|
|
199
|
+
self._hud_last_refresh = now
|
|
200
|
+
try:
|
|
201
|
+
fn()
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# ------------------------------------------------------------------ content
|
|
206
|
+
def on_user_message(self, text: str) -> None:
|
|
207
|
+
# The app echoes the user's line instantly on submit; here we just open a
|
|
208
|
+
# fresh assistant block for the turn that is starting.
|
|
209
|
+
self._t.begin_turn()
|
|
210
|
+
# Turn boundary: drop argument previews stranded by an interrupted turn
|
|
211
|
+
# (a cancelled turn's on_tool_end returns before popping its entry).
|
|
212
|
+
self._tool_details.clear()
|
|
213
|
+
# And drop any subagent identity stranded the same way, so the footer
|
|
214
|
+
# badge can never claim a subagent from an abandoned turn is still active.
|
|
215
|
+
self.clear_subagent_activity()
|
|
216
|
+
|
|
217
|
+
def on_assistant_token(self, delta: str) -> None:
|
|
218
|
+
if _worker_cancelled():
|
|
219
|
+
return
|
|
220
|
+
self._t.stream_assistant(delta)
|
|
221
|
+
|
|
222
|
+
def on_reasoning_start(self, block_id: str) -> None:
|
|
223
|
+
"""Open one provider-call-scoped safe-summary block."""
|
|
224
|
+
|
|
225
|
+
if _worker_cancelled() or self._trace_level == "off":
|
|
226
|
+
return
|
|
227
|
+
self._t.begin_reasoning(block_id)
|
|
228
|
+
|
|
229
|
+
def on_reasoning_token(self, delta: str) -> None:
|
|
230
|
+
# Provider adapters route only safe, provider-generated summaries here;
|
|
231
|
+
# raw, encrypted, and redacted reasoning never reaches the surface.
|
|
232
|
+
if _worker_cancelled() or self._trace_level == "off":
|
|
233
|
+
return
|
|
234
|
+
self._t.stream_reasoning(delta)
|
|
235
|
+
|
|
236
|
+
def on_reasoning_end(self, block_id: str) -> None:
|
|
237
|
+
if _worker_cancelled():
|
|
238
|
+
return
|
|
239
|
+
self._t.end_reasoning(block_id)
|
|
240
|
+
|
|
241
|
+
def reset_streamed_assistant(self) -> None:
|
|
242
|
+
# Transport retry restreams the reply; wipe the live block so the
|
|
243
|
+
# abandoned generation never shows doubled (see core._on_stream_restart).
|
|
244
|
+
self._t.restart_assistant()
|
|
245
|
+
|
|
246
|
+
def on_assistant_message_done(self, text: str) -> None:
|
|
247
|
+
if _worker_cancelled():
|
|
248
|
+
return
|
|
249
|
+
self._t.finish_assistant(text or "")
|
|
250
|
+
# A model response just landed (usage recorded) — refresh the live HUD so a
|
|
251
|
+
# multi-step turn's bottom-right numbers advance, not just at the very end.
|
|
252
|
+
self._maybe_refresh_hud()
|
|
253
|
+
|
|
254
|
+
def on_progress_update(self, message: str) -> None:
|
|
255
|
+
if _worker_cancelled() or self._trace_level == "off":
|
|
256
|
+
return
|
|
257
|
+
self._t.set_status(_truncate(message, limit=80) or None)
|
|
258
|
+
|
|
259
|
+
def on_status_update(self, status: StatusEvent) -> None:
|
|
260
|
+
# Static workspace/model/mode line — the footer already carries this.
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
# ------------------------------------------------------------- Forge swarm
|
|
264
|
+
def begin_forge(self, paths: object, token: object | None = None) -> None:
|
|
265
|
+
"""Open the live Forge execution view from the current plan on disk.
|
|
266
|
+
|
|
267
|
+
Called by the worker just before ``run_swarm`` so the task table shows the
|
|
268
|
+
initial statuses; the swarm then refreshes them through
|
|
269
|
+
:meth:`on_swarm_event` and the final pass in :meth:`end_forge`. ``token`` is
|
|
270
|
+
the worker's cancellation token — checked in :meth:`on_swarm_event` (which
|
|
271
|
+
runs on the trace sink's OWN thread, where the thread-local
|
|
272
|
+
``_worker_cancelled`` would not see it) so a soft-interrupt stops painting."""
|
|
273
|
+
self._forge_paths = paths
|
|
274
|
+
self._forge_token = token
|
|
275
|
+
self._forge_last_refresh = 0.0
|
|
276
|
+
self._forge_run_id = str(getattr(paths, "run_id", "") or "")
|
|
277
|
+
self._t.forge_begin(self._forge_run_id, list(self._read_plan_tasks()))
|
|
278
|
+
|
|
279
|
+
def _forge_run_cancelled(self) -> bool:
|
|
280
|
+
token = self._forge_token
|
|
281
|
+
return bool(token is not None and getattr(token, "is_cancelled", False))
|
|
282
|
+
|
|
283
|
+
def on_swarm_event(self, event: object) -> None:
|
|
284
|
+
"""Receive a structured swarm trace event (preferred by the serialized
|
|
285
|
+
sink over :meth:`on_progress_update`): stream it as a trace/error line,
|
|
286
|
+
highlight the in-flight task, and refresh statuses from plan.json.
|
|
287
|
+
|
|
288
|
+
Runs on the trace sink's daemon thread, so it guards on the worker's token
|
|
289
|
+
(not the thread-local cancel flag), on the view still being open (a new turn
|
|
290
|
+
clears it), and on the event's run id matching — so a soft-interrupted or
|
|
291
|
+
superseded run can never paint stale progress."""
|
|
292
|
+
if self._forge_run_cancelled():
|
|
293
|
+
return
|
|
294
|
+
if self._t.forge_snapshot() is None:
|
|
295
|
+
return # the view was cleared (e.g. a new turn started)
|
|
296
|
+
event_run_id = str(getattr(event, "run_id", "") or "")
|
|
297
|
+
if event_run_id and self._forge_run_id and event_run_id != self._forge_run_id:
|
|
298
|
+
return # stale event from a previous run
|
|
299
|
+
phase = str(getattr(event, "phase", "") or "")
|
|
300
|
+
message = str(getattr(event, "message", "") or "")
|
|
301
|
+
task_id = getattr(event, "task_id", None)
|
|
302
|
+
task_id = str(task_id) if task_id else None
|
|
303
|
+
# Keep the run CLEAN: the live task table + the single moving phase line tell
|
|
304
|
+
# the story, so progress events are NOT spilled as a wall of trace lines.
|
|
305
|
+
# Only errors are surfaced as persistent lines (they matter and are rare).
|
|
306
|
+
if "error" in phase.lower():
|
|
307
|
+
prefix = f"[{task_id}] " if task_id else ""
|
|
308
|
+
self._t.append(
|
|
309
|
+
"error", f"✗ {_truncate(f'{prefix}{message}'.strip() or phase, limit=200)}"
|
|
310
|
+
)
|
|
311
|
+
self._t.forge_set_active(task_id, phase=phase, message=message)
|
|
312
|
+
self._maybe_refresh_forge_statuses()
|
|
313
|
+
|
|
314
|
+
def end_forge(self, summary: str = "", *, paths: object | None = None) -> None:
|
|
315
|
+
"""Apply the final task statuses from disk (adding any tasks enrichment added
|
|
316
|
+
mid-run) and freeze the view as done, with a concise outcome summary.
|
|
317
|
+
|
|
318
|
+
``paths`` re-arms the run paths for a late finalize: a soft-interrupt
|
|
319
|
+
clears ``self._forge_paths`` (via :meth:`interrupt_forge`) even though
|
|
320
|
+
the swarm cannot actually be stopped — when it then runs to its natural
|
|
321
|
+
end, the worker passes the run's paths back so the frozen table shows
|
|
322
|
+
the statuses the finished swarm really wrote instead of an empty view."""
|
|
323
|
+
if paths is not None:
|
|
324
|
+
self._forge_paths = paths
|
|
325
|
+
tasks = self._read_plan_tasks()
|
|
326
|
+
self._t.forge_sync_tasks(tasks)
|
|
327
|
+
done, failed, remaining = forge_status_counts(status for _tid, _title, status in tasks)
|
|
328
|
+
ok = failed == 0 and remaining == 0
|
|
329
|
+
if not summary:
|
|
330
|
+
if not tasks:
|
|
331
|
+
summary = "No execution-ready tasks."
|
|
332
|
+
else:
|
|
333
|
+
parts = [f"{done} done"]
|
|
334
|
+
if failed:
|
|
335
|
+
parts.append(f"{failed} failed")
|
|
336
|
+
if remaining:
|
|
337
|
+
parts.append(f"{remaining} remaining")
|
|
338
|
+
summary = "Done · " + " · ".join(parts) if ok else "Finished · " + " · ".join(parts)
|
|
339
|
+
self._t.forge_finish({tid: status for tid, _title, status in tasks}, summary, ok=ok)
|
|
340
|
+
self._forge_paths = None
|
|
341
|
+
self._forge_token = None
|
|
342
|
+
self._forge_run_id = ""
|
|
343
|
+
|
|
344
|
+
def interrupt_forge(self, summary: str = "Interrupted.") -> None:
|
|
345
|
+
"""Mark any live in-progress Forge tasks interrupted and freeze the view."""
|
|
346
|
+
path = getattr(self._forge_paths, "plan_json_path", None)
|
|
347
|
+
updated: dict[str, str] = {}
|
|
348
|
+
if path is not None:
|
|
349
|
+
try:
|
|
350
|
+
import json
|
|
351
|
+
from datetime import datetime, timezone
|
|
352
|
+
|
|
353
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
354
|
+
changed = False
|
|
355
|
+
for task in data.get("tasks") or []:
|
|
356
|
+
if not isinstance(task, dict):
|
|
357
|
+
continue
|
|
358
|
+
status = str(task.get("status") or "").strip().lower()
|
|
359
|
+
task_id = str(task.get("id") or "").strip()
|
|
360
|
+
if not task_id or status not in FORGE_RUNNING_STATES:
|
|
361
|
+
continue
|
|
362
|
+
task["status"] = "interrupted"
|
|
363
|
+
task["last_error"] = "Interrupted by user."
|
|
364
|
+
updated[task_id] = "interrupted"
|
|
365
|
+
changed = True
|
|
366
|
+
if changed:
|
|
367
|
+
data["updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
368
|
+
path.write_text(
|
|
369
|
+
json.dumps(data, indent=2, sort_keys=False) + "\n",
|
|
370
|
+
encoding="utf-8",
|
|
371
|
+
)
|
|
372
|
+
except Exception:
|
|
373
|
+
updated = {}
|
|
374
|
+
tasks = self._read_plan_tasks()
|
|
375
|
+
if tasks:
|
|
376
|
+
self._t.forge_sync_tasks(tasks)
|
|
377
|
+
status_map = {tid: status for tid, _title, status in tasks}
|
|
378
|
+
else:
|
|
379
|
+
status_map = dict(updated)
|
|
380
|
+
if status_map:
|
|
381
|
+
self._t.forge_update_statuses(status_map)
|
|
382
|
+
self._t.forge_finish(status_map, summary, ok=False)
|
|
383
|
+
self._forge_paths = None
|
|
384
|
+
self._forge_token = None
|
|
385
|
+
self._forge_run_id = ""
|
|
386
|
+
|
|
387
|
+
def append_system(self, text: str) -> None:
|
|
388
|
+
"""Append captured handler output (warnings / summary) as one system block."""
|
|
389
|
+
clean = str(text or "").rstrip()
|
|
390
|
+
if clean:
|
|
391
|
+
self._t.append("system", clean)
|
|
392
|
+
|
|
393
|
+
def append_note(self, text: str, *, role: str = "system") -> None:
|
|
394
|
+
"""Append a single status line with an explicit role.
|
|
395
|
+
|
|
396
|
+
Used for the ``/resume`` outcome line so the caller can pick a role that
|
|
397
|
+
flips the welcome→chat pane when needed (a resumed session with no visible
|
|
398
|
+
history leaves the transcript empty, where a ``system`` line stays hidden
|
|
399
|
+
behind the landing screen but an ``assistant`` line surfaces it)."""
|
|
400
|
+
clean = str(text or "").rstrip()
|
|
401
|
+
if clean:
|
|
402
|
+
self._t.append(str(role or "system"), clean)
|
|
403
|
+
|
|
404
|
+
def replace_history(self, messages: list[dict[str, object]]) -> None:
|
|
405
|
+
"""Reload the transcript with a resumed conversation's history.
|
|
406
|
+
|
|
407
|
+
Called (on the UI thread, no turn in flight) after ``/resume`` swaps the
|
|
408
|
+
live session: the prior conversation's user/assistant turns replace the
|
|
409
|
+
current pane so the user actually "enters" the resumed session instead of
|
|
410
|
+
keeping the previous transcript. Tool calls/results are dropped by
|
|
411
|
+
:meth:`TuiTranscript.load_history` so the view stays clean."""
|
|
412
|
+
self._t.load_history(messages or [])
|
|
413
|
+
|
|
414
|
+
def _maybe_refresh_forge_statuses(self) -> None:
|
|
415
|
+
import time
|
|
416
|
+
|
|
417
|
+
now = time.monotonic()
|
|
418
|
+
if now - self._forge_last_refresh < 0.4:
|
|
419
|
+
return
|
|
420
|
+
self._forge_last_refresh = now
|
|
421
|
+
tasks = self._read_plan_tasks()
|
|
422
|
+
if tasks:
|
|
423
|
+
# Sync (not just update) so tasks added by plan enrichment mid-run show up.
|
|
424
|
+
self._t.forge_sync_tasks(tasks)
|
|
425
|
+
|
|
426
|
+
def _read_plan_tasks(self) -> list[tuple[str, str, str]]:
|
|
427
|
+
"""Read ``(id, title, status)`` for each task from plan.json (best-effort).
|
|
428
|
+
|
|
429
|
+
Reads the raw JSON rather than the validating ``load_plan`` so a mid-run
|
|
430
|
+
refresh stays cheap and never raises while the swarm rewrites the file."""
|
|
431
|
+
paths = self._forge_paths
|
|
432
|
+
path = getattr(paths, "plan_json_path", None)
|
|
433
|
+
if path is None:
|
|
434
|
+
return []
|
|
435
|
+
try:
|
|
436
|
+
import json
|
|
437
|
+
|
|
438
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
439
|
+
except Exception:
|
|
440
|
+
return []
|
|
441
|
+
out: list[tuple[str, str, str]] = []
|
|
442
|
+
for task in data.get("tasks") or []:
|
|
443
|
+
if isinstance(task, dict) and task.get("id"):
|
|
444
|
+
out.append(
|
|
445
|
+
(
|
|
446
|
+
str(task["id"]),
|
|
447
|
+
str(task.get("title") or ""),
|
|
448
|
+
str(task.get("status") or "planned"),
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
return out
|
|
452
|
+
|
|
453
|
+
# -------------------------------------------------------------------- tools
|
|
454
|
+
def on_tool_start(self, event: ToolStartEvent) -> None:
|
|
455
|
+
# No "⚙ start" line — the live activity indicator under the question shows
|
|
456
|
+
# the running tool (with a spinner + elapsed); it commits to a "✓ …" line
|
|
457
|
+
# on completion. Collapse any open thinking first.
|
|
458
|
+
if _worker_cancelled():
|
|
459
|
+
return
|
|
460
|
+
label = _tool_label(event.name)
|
|
461
|
+
arg_detail = _tool_detail(event.name, event.args)
|
|
462
|
+
self._t.end_reasoning()
|
|
463
|
+
if self._trace_level == "off":
|
|
464
|
+
self._t.set_status("Working…")
|
|
465
|
+
return
|
|
466
|
+
if arg_detail and event.tool_call_id:
|
|
467
|
+
# Stale-entry backstop: evict oldest first, never in-flight wholesale.
|
|
468
|
+
# Stored for nested calls too, so a nested FAILURE's ✗ line can say
|
|
469
|
+
# which invocation failed (its success line is suppressed anyway).
|
|
470
|
+
while len(self._tool_details) > 256:
|
|
471
|
+
self._tool_details.pop(next(iter(self._tool_details)))
|
|
472
|
+
self._tool_details[event.tool_call_id] = arg_detail
|
|
473
|
+
if event.subagent_name and self._trace_level != "full":
|
|
474
|
+
# A nested subagent's tool call: the entry line already said who is
|
|
475
|
+
# working and on what, so its step-by-step chatter stays out of the
|
|
476
|
+
# transcript — the live status alone shows what the agent is doing
|
|
477
|
+
# right now. Trace "full" opts back into the committed lines.
|
|
478
|
+
detail = f" · {arg_detail}" if arg_detail else ""
|
|
479
|
+
display_name = self._current_subagent_display_name(str(event.subagent_name))
|
|
480
|
+
followed_label = self._current_subagent_follow_label(str(event.subagent_name))
|
|
481
|
+
activity = _truncate(f"{label}{detail}", limit=64)
|
|
482
|
+
self._subagent_activity[display_name] = activity
|
|
483
|
+
self._t.set_status(_truncate(f"↪ {followed_label} ▸ {activity}…", limit=80))
|
|
484
|
+
return
|
|
485
|
+
if self._trace_level == "full":
|
|
486
|
+
detail = f" · {arg_detail}" if arg_detail else ""
|
|
487
|
+
self._t.append("trace", f"▸ {label}{detail}")
|
|
488
|
+
self._last_tool_trace_name = None
|
|
489
|
+
# Keep the transient status short so it stays on one row on narrow
|
|
490
|
+
# terminals; the committed "✓" line carries the fuller preview.
|
|
491
|
+
status_detail = arg_detail if len(arg_detail) <= 40 else arg_detail[:39] + "…"
|
|
492
|
+
self._t.set_status(f"{label} · {status_detail}…" if status_detail else f"{label}…")
|
|
493
|
+
|
|
494
|
+
def on_tool_output(self, event: ToolOutputEvent) -> None:
|
|
495
|
+
return
|
|
496
|
+
|
|
497
|
+
def on_tool_end(self, event: ToolEndEvent) -> None:
|
|
498
|
+
# Pop before the cancellation check so an interrupted turn cannot strand
|
|
499
|
+
# the entry (and a reused call id can never show a stale preview).
|
|
500
|
+
arg_detail = self._tool_details.pop(event.tool_call_id, "")
|
|
501
|
+
if _worker_cancelled():
|
|
502
|
+
return
|
|
503
|
+
label = _tool_label(event.name)
|
|
504
|
+
if arg_detail:
|
|
505
|
+
label = f"{label} · {arg_detail}"
|
|
506
|
+
elapsed = _format_ms(event.elapsed_ms)
|
|
507
|
+
suffix = f" ({elapsed})" if elapsed else ""
|
|
508
|
+
if event.subagent_name and self._trace_level != "full":
|
|
509
|
+
# Nested subagent steps stay out of the transcript (minimal view) —
|
|
510
|
+
# except failures, which are real signal and keep their ✗ line,
|
|
511
|
+
# attributed to the agent that hit them. Successes just roll the
|
|
512
|
+
# live status back to "working" until the ↩ end line closes the run.
|
|
513
|
+
display_name = self._current_subagent_display_name(str(event.subagent_name))
|
|
514
|
+
followed_label = self._current_subagent_follow_label(str(event.subagent_name))
|
|
515
|
+
if event.status != "done":
|
|
516
|
+
meta = event.meta if isinstance(event.meta, dict) else {}
|
|
517
|
+
verdict = "approval declined" if meta.get("approval_declined") else "failed"
|
|
518
|
+
err = str(meta.get("error") or "")
|
|
519
|
+
detail = f": {_truncate(err)}" if err else ""
|
|
520
|
+
self._t.append("error", f"✗ {display_name} ▸ {label} {verdict}{suffix}{detail}")
|
|
521
|
+
if self._trace_level == "off":
|
|
522
|
+
# Off is quiet everywhere else — no named identity in the status
|
|
523
|
+
# (mirrors on_subagent_start's off branch and the pre-feature
|
|
524
|
+
# None-after-every-tool-end behavior).
|
|
525
|
+
self._t.set_status(None)
|
|
526
|
+
else:
|
|
527
|
+
outcome = "complete" if event.status == "done" else "failed"
|
|
528
|
+
activity = _truncate(f"{label} · {outcome}", limit=64)
|
|
529
|
+
self._subagent_activity[display_name] = activity
|
|
530
|
+
self._t.set_status(f"↪ {followed_label} · {activity}…")
|
|
531
|
+
self._maybe_refresh_hud()
|
|
532
|
+
return
|
|
533
|
+
if event.status == "done" and self._trace_level != "off":
|
|
534
|
+
# Consecutive successes of the SAME tool group under one header:
|
|
535
|
+
# ✓ Search Web · current date today (1.5s)
|
|
536
|
+
# ↳ today's date (352ms)
|
|
537
|
+
# The adjacency check keeps grouping honest — anything else appended
|
|
538
|
+
# in between (a message, an error, another tool) restarts a full line.
|
|
539
|
+
entries = self._t.entries
|
|
540
|
+
grouped = (
|
|
541
|
+
arg_detail != ""
|
|
542
|
+
and self._last_tool_trace_name == event.name
|
|
543
|
+
and bool(entries)
|
|
544
|
+
and entries[-1][0] == "trace"
|
|
545
|
+
and (entries[-1][1].startswith("✓") or entries[-1][1].startswith(" ↳"))
|
|
546
|
+
)
|
|
547
|
+
if grouped:
|
|
548
|
+
self._t.append("trace", f" ↳ {arg_detail}{suffix}")
|
|
549
|
+
else:
|
|
550
|
+
self._t.append("trace", f"✓ {label}{suffix}")
|
|
551
|
+
self._last_tool_trace_name = event.name
|
|
552
|
+
elif isinstance(event.meta, dict) and event.meta.get("approval_declined"):
|
|
553
|
+
err = str(event.meta.get("error") or "")
|
|
554
|
+
detail = f": {_truncate(err)}" if err else ""
|
|
555
|
+
self._t.append("error", f"✗ {label} approval declined{suffix}{detail}")
|
|
556
|
+
else:
|
|
557
|
+
err = ""
|
|
558
|
+
if isinstance(event.meta, dict):
|
|
559
|
+
err = str(event.meta.get("error") or "")
|
|
560
|
+
detail = f": {_truncate(err)}" if err else ""
|
|
561
|
+
self._t.append("error", f"✗ {label} failed{suffix}{detail}")
|
|
562
|
+
self._t.set_status(None)
|
|
563
|
+
# Between steps (no concurrent message mutation on this worker thread) is a
|
|
564
|
+
# safe point to advance the footer HUD as the turn progresses.
|
|
565
|
+
self._maybe_refresh_hud()
|
|
566
|
+
|
|
567
|
+
# --------------------------------------------------------------- subagents
|
|
568
|
+
def _sync_subagent_badge(self) -> None:
|
|
569
|
+
"""Tell the app which live subagents to pin in the footer."""
|
|
570
|
+
names = tuple(
|
|
571
|
+
self._subagent_display_name(name, label, workspace_view)
|
|
572
|
+
for _thread_id, name, label, workspace_view in self._subagent_stack
|
|
573
|
+
)
|
|
574
|
+
fn = self._on_subagent_activity
|
|
575
|
+
if fn is not None:
|
|
576
|
+
try:
|
|
577
|
+
fn(names[-1] if names else None)
|
|
578
|
+
except Exception:
|
|
579
|
+
pass
|
|
580
|
+
all_fn = self._on_subagent_activities
|
|
581
|
+
if all_fn is not None:
|
|
582
|
+
try:
|
|
583
|
+
all_fn(names)
|
|
584
|
+
except Exception:
|
|
585
|
+
pass
|
|
586
|
+
|
|
587
|
+
@staticmethod
|
|
588
|
+
def _subagent_display_name(name: str, label: str, workspace_view: str) -> str:
|
|
589
|
+
marker = "[iso] " if workspace_view in {"isolated", "pinned"} else ""
|
|
590
|
+
return f"{marker}{subagent_identity(name, label)}"
|
|
591
|
+
|
|
592
|
+
def _current_subagent_display_name(self, name: str) -> str:
|
|
593
|
+
thread_id = threading.get_ident()
|
|
594
|
+
for owner_thread, candidate, label, workspace_view in reversed(self._subagent_stack):
|
|
595
|
+
if owner_thread == thread_id and candidate == name:
|
|
596
|
+
return self._subagent_display_name(candidate, label, workspace_view)
|
|
597
|
+
return name
|
|
598
|
+
|
|
599
|
+
@staticmethod
|
|
600
|
+
def _subagent_follow_label(name: str, label: str, workspace_view: str) -> str:
|
|
601
|
+
marker = "[iso] " if workspace_view in {"isolated", "pinned"} else ""
|
|
602
|
+
if label:
|
|
603
|
+
return f"{marker}{name} · following {label}"
|
|
604
|
+
return f"following {marker}{name}"
|
|
605
|
+
|
|
606
|
+
def _current_subagent_follow_label(self, name: str) -> str:
|
|
607
|
+
thread_id = threading.get_ident()
|
|
608
|
+
for owner_thread, candidate, label, workspace_view in reversed(self._subagent_stack):
|
|
609
|
+
if owner_thread == thread_id and candidate == name:
|
|
610
|
+
return self._subagent_follow_label(candidate, label, workspace_view)
|
|
611
|
+
return name
|
|
612
|
+
|
|
613
|
+
def clear_subagent_activity(self) -> None:
|
|
614
|
+
"""Forget every live subagent and clear the footer badge.
|
|
615
|
+
|
|
616
|
+
Called at turn boundaries and on soft-interrupt. After this, the
|
|
617
|
+
abandoned runs' late end events find no matching stack entry and are
|
|
618
|
+
dropped whole — they cannot pop, restyle, or re-pin anything the next
|
|
619
|
+
turn owns."""
|
|
620
|
+
if self._subagent_stack:
|
|
621
|
+
self._subagent_stack.clear()
|
|
622
|
+
self._sync_subagent_badge()
|
|
623
|
+
self._subagent_activity.clear()
|
|
624
|
+
|
|
625
|
+
def on_subagent_start(self, event: SubagentStartEvent) -> None:
|
|
626
|
+
if _worker_cancelled():
|
|
627
|
+
return
|
|
628
|
+
run_started = self._on_subagent_run_started
|
|
629
|
+
if run_started is not None and event.subagent_run_id:
|
|
630
|
+
try:
|
|
631
|
+
run_started(event)
|
|
632
|
+
except Exception:
|
|
633
|
+
pass
|
|
634
|
+
# The badge is identity, not trace — it shows at every trace level.
|
|
635
|
+
name = str(event.name)
|
|
636
|
+
label = str(event.label or "")
|
|
637
|
+
workspace_view = str(event.workspace_view or "shared")
|
|
638
|
+
self._subagent_stack.append((threading.get_ident(), name, label, workspace_view))
|
|
639
|
+
self._sync_subagent_badge()
|
|
640
|
+
display_name = self._subagent_display_name(name, label, workspace_view)
|
|
641
|
+
followed_label = self._subagent_follow_label(name, label, workspace_view)
|
|
642
|
+
self._subagent_activity[display_name] = "starting"
|
|
643
|
+
if self._trace_level == "off":
|
|
644
|
+
return
|
|
645
|
+
# The spawn line carries stable identity only. Live activity is derived
|
|
646
|
+
# from the child's actual tool events below, never from canned role text.
|
|
647
|
+
self._t.append("subagent", f"↪ {display_name} · {event.mode}")
|
|
648
|
+
self._t.set_status(f"↪ {followed_label} · starting…")
|
|
649
|
+
|
|
650
|
+
def on_subagent_end(self, event: SubagentEndEvent) -> None:
|
|
651
|
+
# A run's start and end fire on the same thread, so the end may only
|
|
652
|
+
# unwind an entry this thread pushed. An end with no matching entry is
|
|
653
|
+
# stale — an abandoned turn's agent finishing late, or a start this
|
|
654
|
+
# surface never recorded — and must not touch the live turn's badge,
|
|
655
|
+
# status, or transcript at all.
|
|
656
|
+
entry = (
|
|
657
|
+
threading.get_ident(),
|
|
658
|
+
str(event.name),
|
|
659
|
+
str(event.label or ""),
|
|
660
|
+
str(event.workspace_view or "shared"),
|
|
661
|
+
)
|
|
662
|
+
matched = False
|
|
663
|
+
for index in range(len(self._subagent_stack) - 1, -1, -1):
|
|
664
|
+
if self._subagent_stack[index] == entry:
|
|
665
|
+
del self._subagent_stack[index]
|
|
666
|
+
matched = True
|
|
667
|
+
break
|
|
668
|
+
if not matched:
|
|
669
|
+
return
|
|
670
|
+
name = entry[1]
|
|
671
|
+
display_name = self._subagent_display_name(name, entry[2], entry[3])
|
|
672
|
+
if all(
|
|
673
|
+
self._subagent_display_name(stacked_name, label, workspace_view) != display_name
|
|
674
|
+
for _tid, stacked_name, label, workspace_view in self._subagent_stack
|
|
675
|
+
):
|
|
676
|
+
self._subagent_activity.pop(display_name, None)
|
|
677
|
+
# Unwind the badge even for a cancelled worker — the matched pop only
|
|
678
|
+
# ever removes this thread's own entry, so re-syncing here can never
|
|
679
|
+
# resurrect a name the interrupt already dropped (clear_subagent_activity
|
|
680
|
+
# emptied the stack, making this pop unreachable for abandoned runs).
|
|
681
|
+
self._sync_subagent_badge()
|
|
682
|
+
if not self._subagent_stack:
|
|
683
|
+
# The last live agent is gone — clear the activity line at every
|
|
684
|
+
# trace level so an "↪ name · …" status can never outlive its run.
|
|
685
|
+
self._t.set_status(None)
|
|
686
|
+
if _worker_cancelled() or self._trace_level == "off":
|
|
687
|
+
return
|
|
688
|
+
status = "finished" if event.status == "success" else event.status
|
|
689
|
+
self._t.append(
|
|
690
|
+
"trace",
|
|
691
|
+
f"↩ {display_name} · {status} · {event.steps_completed} steps",
|
|
692
|
+
)
|
|
693
|
+
if self._subagent_stack:
|
|
694
|
+
# Roll the activity line to a survivor so the status is never
|
|
695
|
+
# attributed to an agent whose ↩ line just printed.
|
|
696
|
+
_owner, top_name, top_label, top_view = self._subagent_stack[-1]
|
|
697
|
+
top = self._subagent_display_name(top_name, top_label, top_view)
|
|
698
|
+
followed_label = self._subagent_follow_label(
|
|
699
|
+
top_name,
|
|
700
|
+
top_label,
|
|
701
|
+
top_view,
|
|
702
|
+
)
|
|
703
|
+
activity = self._subagent_activity.get(top, "starting")
|
|
704
|
+
self._t.set_status(f"↪ {followed_label} · {activity}…")
|
|
705
|
+
|
|
706
|
+
def on_patch_generated(self, event: PatchEvent) -> None:
|
|
707
|
+
if _worker_cancelled() or self._trace_level == "off":
|
|
708
|
+
return
|
|
709
|
+
files = ", ".join(event.files[:5]) or "patch"
|
|
710
|
+
self._t.append("trace", f"✎ patch · {files}")
|
|
711
|
+
|
|
712
|
+
# ---------------------------------------------------------------- diagnostics
|
|
713
|
+
def on_error(self, err: str) -> None:
|
|
714
|
+
if _worker_cancelled():
|
|
715
|
+
return
|
|
716
|
+
self._t.append("error", _truncate(friendly_llm_error_message(err), limit=480) or "error")
|
|
717
|
+
self._t.set_status(None)
|
|
718
|
+
|
|
719
|
+
def on_warning(self, warning: str) -> None:
|
|
720
|
+
if _worker_cancelled():
|
|
721
|
+
return
|
|
722
|
+
self._t.append("warn", _truncate(str(warning), limit=480) or "warning")
|
|
723
|
+
|
|
724
|
+
# ------------------------------------------------------------------ approval
|
|
725
|
+
def request_approval(self, request: ApprovalRequest) -> ApprovalDecision:
|
|
726
|
+
# Reaching this method already means the execution mode decided this
|
|
727
|
+
# action needs a human: ``fullaccess`` returns before requesting approval
|
|
728
|
+
# and ``readonly`` raises. So the surface never second-guesses the gate —
|
|
729
|
+
# it prompts, or fails closed when it cannot. (There used to be an
|
|
730
|
+
# auto-approve short-circuit here; it let a Shift+Tab silently turn
|
|
731
|
+
# ``review`` into ``fullaccess`` while the footer still read "safe".)
|
|
732
|
+
if _worker_cancelled():
|
|
733
|
+
# The user interrupted this (now-abandoned) turn; deny without prompting.
|
|
734
|
+
return ApprovalDecision(allow=False)
|
|
735
|
+
if self._approval_ui is not None:
|
|
736
|
+
try:
|
|
737
|
+
decision = self._approval_ui(request)
|
|
738
|
+
except Exception:
|
|
739
|
+
decision = None
|
|
740
|
+
if isinstance(decision, ApprovalDecision):
|
|
741
|
+
return decision
|
|
742
|
+
# No interactive approver reachable → fail closed.
|
|
743
|
+
self._t.append(
|
|
744
|
+
"warn",
|
|
745
|
+
f"Denied {request.kind}: approval required "
|
|
746
|
+
"(this mode gates the action, but no approval UI is available).",
|
|
747
|
+
)
|
|
748
|
+
return ApprovalDecision(allow=False)
|
|
749
|
+
|
|
750
|
+
# ----------------------------------------------------------------- emit path
|
|
751
|
+
# The agent dual-emits: message/tool events go through additive ``emit_*``
|
|
752
|
+
# helpers (skipped when absent) AND the legacy ``on_*`` calls we implement
|
|
753
|
+
# above — so we deliberately do NOT define ``emit_message_*`` /
|
|
754
|
+
# ``emit_tool_call_*`` (that would double-render). Error/warning/info, by
|
|
755
|
+
# contrast, are EITHER/OR: the runtime prefers a real (non-Noop) ``emit_*``
|
|
756
|
+
# and only falls back to ``on_*`` otherwise. We therefore define real
|
|
757
|
+
# delegating handlers so those never get silently dropped.
|
|
758
|
+
#
|
|
759
|
+
# Note: do not add a catch-all ``__getattr__`` here — a synthesized no-op
|
|
760
|
+
# ``emit_error``/``emit_warning`` looks "real" to the runtime's capability
|
|
761
|
+
# probe and would swallow every error/warning in the TUI.
|
|
762
|
+
def emit_error(
|
|
763
|
+
self,
|
|
764
|
+
code: str,
|
|
765
|
+
message: str,
|
|
766
|
+
recoverable: bool,
|
|
767
|
+
*,
|
|
768
|
+
worker_id: str | None = None,
|
|
769
|
+
role: str | None = None,
|
|
770
|
+
) -> None:
|
|
771
|
+
prefix = f"{code}: " if code else ""
|
|
772
|
+
suffix = "" if recoverable else " (not recoverable)"
|
|
773
|
+
self.on_error(f"{prefix}{message}{suffix}")
|
|
774
|
+
|
|
775
|
+
def emit_warning(
|
|
776
|
+
self,
|
|
777
|
+
message: str,
|
|
778
|
+
*,
|
|
779
|
+
worker_id: str | None = None,
|
|
780
|
+
role: str | None = None,
|
|
781
|
+
) -> None:
|
|
782
|
+
self.on_warning(str(message))
|
|
783
|
+
|
|
784
|
+
def emit_info(
|
|
785
|
+
self,
|
|
786
|
+
message: str,
|
|
787
|
+
*,
|
|
788
|
+
worker_id: str | None = None,
|
|
789
|
+
role: str | None = None,
|
|
790
|
+
) -> None:
|
|
791
|
+
if _worker_cancelled():
|
|
792
|
+
return
|
|
793
|
+
self._t.append("info", _truncate(str(message), limit=480) or "info")
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
__all__ = ["TuiSurface", "set_active_cancellation"]
|