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,1018 @@
|
|
|
1
|
+
"""TUI-native panel spec builders for chat slash commands.
|
|
2
|
+
|
|
3
|
+
Each builder mirrors a classic ``_print_*`` / ``_chat_*_panel`` command but
|
|
4
|
+
returns a structured *spec* the full-screen TUI renders as a centered popup
|
|
5
|
+
panel (via ``_render_kv_panel_rows`` in :mod:`...cli_impl.tui.app`) instead of
|
|
6
|
+
dumping flat, colourless captured Rich text into the transcript.
|
|
7
|
+
|
|
8
|
+
A panel spec is::
|
|
9
|
+
|
|
10
|
+
{"title": str, "hint"?: str, "sections": [(section_name, rows)]}
|
|
11
|
+
|
|
12
|
+
where ``rows`` is a list of ``(key, value, tone)`` tuples. ``tone`` selects the
|
|
13
|
+
value colour: ``"accent"`` (on/healthy, green), ``"plain"`` (neutral),
|
|
14
|
+
``"warn"`` (amber), ``"err"`` (red), ``"dim"`` (muted).
|
|
15
|
+
|
|
16
|
+
Unlike the sibling legacy split modules, this one imports every helper it needs
|
|
17
|
+
*explicitly* (no ``from .cli_common import *`` global injection), so the builders
|
|
18
|
+
are importable and unit-testable in isolation with a plain fake session.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from ..tui.forge_status import forge_status_bucket, forge_status_glyph
|
|
26
|
+
|
|
27
|
+
# Canonical, self-contained helpers (defined directly in these modules — not
|
|
28
|
+
# injected at runtime), so the imports resolve at import time and in tests.
|
|
29
|
+
from .startup import (
|
|
30
|
+
_chat_context_percent_value,
|
|
31
|
+
_chat_usage_hud_enabled,
|
|
32
|
+
_format_chat_context_percent,
|
|
33
|
+
_format_exact_token_count,
|
|
34
|
+
_format_reported_token_total,
|
|
35
|
+
_format_usage_billing_for_display,
|
|
36
|
+
_format_usage_source_for_display,
|
|
37
|
+
_known_cost_value,
|
|
38
|
+
_refresh_chat_hud_context_cache,
|
|
39
|
+
_session_skill_listing,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
PanelSpec = dict[str, Any]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_terminal_runtime_s(runtime_s: float) -> str:
|
|
46
|
+
"""Mirror of the classic terminal runtime formatter (kept local so this
|
|
47
|
+
module never imports the injected ``chat/commands.py`` legacy module)."""
|
|
48
|
+
if runtime_s < 0:
|
|
49
|
+
runtime_s = 0.0
|
|
50
|
+
if runtime_s < 60:
|
|
51
|
+
return f"{runtime_s:.1f}s"
|
|
52
|
+
minutes = int(runtime_s // 60)
|
|
53
|
+
seconds = int(runtime_s % 60)
|
|
54
|
+
return f"{minutes}m{seconds:02d}s"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------- /usage
|
|
58
|
+
def _chat_usage_panel_spec(*, session: Any) -> PanelSpec:
|
|
59
|
+
"""Token count & cost as a panel (mirrors ``_print_chat_usage``)."""
|
|
60
|
+
summary = getattr(session, "usage_summary", None)
|
|
61
|
+
if summary is None:
|
|
62
|
+
return {
|
|
63
|
+
"title": "Usage",
|
|
64
|
+
"sections": [
|
|
65
|
+
("Usage", [("status", "Usage tracking unavailable for this session.", "plain")])
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
try:
|
|
69
|
+
rows = summary.by_model_rows()
|
|
70
|
+
except Exception: # noqa: BLE001 - never crash the UI on a panel
|
|
71
|
+
rows = []
|
|
72
|
+
if not rows:
|
|
73
|
+
hud = "on" if _chat_usage_hud_enabled(session) else "off"
|
|
74
|
+
return {
|
|
75
|
+
"title": "Usage",
|
|
76
|
+
"sections": [
|
|
77
|
+
(
|
|
78
|
+
"Usage",
|
|
79
|
+
[
|
|
80
|
+
("status", "No usage events yet in this session.", "plain"),
|
|
81
|
+
("hud", hud, "accent" if hud == "on" else "plain"),
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
],
|
|
85
|
+
"hint": "/usage hud on|off to toggle the toolbar HUD · Esc close",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Index keys (not the model name) so a long, namespaced model id never blows
|
|
89
|
+
# past the panel's narrow key column — the full name lives in the wrap-safe
|
|
90
|
+
# value column instead.
|
|
91
|
+
model_rows: list[tuple[str, str, str]] = []
|
|
92
|
+
for idx, row in enumerate(rows, start=1):
|
|
93
|
+
cost = _format_usage_billing_for_display(row, compact=True)
|
|
94
|
+
source = _format_usage_source_for_display(row)
|
|
95
|
+
cache_read = _format_reported_token_total(
|
|
96
|
+
row,
|
|
97
|
+
token_key="cache_read_input_tokens",
|
|
98
|
+
reported_calls_key="cache_read_reported_calls",
|
|
99
|
+
)
|
|
100
|
+
cache_write = _format_reported_token_total(
|
|
101
|
+
row,
|
|
102
|
+
token_key="cache_creation_input_tokens",
|
|
103
|
+
reported_calls_key="cache_creation_reported_calls",
|
|
104
|
+
)
|
|
105
|
+
# A provider that reports no cache figures at all says nothing useful by
|
|
106
|
+
# repeating "not reported" twice on every row; the /usage table still
|
|
107
|
+
# spells the state out per column.
|
|
108
|
+
cache_bits = (
|
|
109
|
+
""
|
|
110
|
+
if cache_read == "not reported" and cache_write == "not reported"
|
|
111
|
+
else f" cache r:{cache_read} w:{cache_write}"
|
|
112
|
+
)
|
|
113
|
+
value = (
|
|
114
|
+
f"{row.get('model') or '-'} "
|
|
115
|
+
f"↓ {_format_exact_token_count(row.get('prompt_tokens'))} "
|
|
116
|
+
f"↑ {_format_exact_token_count(row.get('completion_tokens'))} "
|
|
117
|
+
f"Σ {_format_exact_token_count(row.get('total_tokens'))} "
|
|
118
|
+
f"{cost}{cache_bits} ({source})"
|
|
119
|
+
)
|
|
120
|
+
model_rows.append((str(idx), value, "plain"))
|
|
121
|
+
|
|
122
|
+
totals = summary.totals()
|
|
123
|
+
unknown_total = int(totals.get("unknown_cost_calls") or 0)
|
|
124
|
+
total_cost = _format_usage_billing_for_display(totals)
|
|
125
|
+
# Only paint the cost green when it is fully known; an unknown/partial total
|
|
126
|
+
# stays neutral (never reads as healthy) — matching the classic "[yellow]Total
|
|
127
|
+
# cost is partial" warning.
|
|
128
|
+
cost_tone = (
|
|
129
|
+
"accent"
|
|
130
|
+
if (
|
|
131
|
+
_known_cost_value(totals) is not None
|
|
132
|
+
and unknown_total == 0
|
|
133
|
+
and int(totals.get("catalog_estimated_cost_calls") or 0) == 0
|
|
134
|
+
)
|
|
135
|
+
else "plain"
|
|
136
|
+
)
|
|
137
|
+
total_rows: list[tuple[str, str, str]] = [
|
|
138
|
+
("processed", _format_exact_token_count(totals.get("total_tokens")), "accent"),
|
|
139
|
+
("input total", _format_exact_token_count(totals.get("prompt_tokens")), "plain"),
|
|
140
|
+
(
|
|
141
|
+
"input uncached",
|
|
142
|
+
_format_reported_token_total(
|
|
143
|
+
totals,
|
|
144
|
+
token_key="input_tokens_uncached",
|
|
145
|
+
reported_calls_key="input_tokens_uncached_reported_calls",
|
|
146
|
+
derived_calls_key="input_tokens_uncached_derived_calls",
|
|
147
|
+
),
|
|
148
|
+
"plain",
|
|
149
|
+
),
|
|
150
|
+
(
|
|
151
|
+
"cache read",
|
|
152
|
+
_format_reported_token_total(
|
|
153
|
+
totals,
|
|
154
|
+
token_key="cache_read_input_tokens",
|
|
155
|
+
reported_calls_key="cache_read_reported_calls",
|
|
156
|
+
),
|
|
157
|
+
"plain",
|
|
158
|
+
),
|
|
159
|
+
(
|
|
160
|
+
"cache write",
|
|
161
|
+
_format_reported_token_total(
|
|
162
|
+
totals,
|
|
163
|
+
token_key="cache_creation_input_tokens",
|
|
164
|
+
reported_calls_key="cache_creation_reported_calls",
|
|
165
|
+
),
|
|
166
|
+
"plain",
|
|
167
|
+
),
|
|
168
|
+
("output", _format_exact_token_count(totals.get("completion_tokens")), "plain"),
|
|
169
|
+
("billing", total_cost, cost_tone),
|
|
170
|
+
(
|
|
171
|
+
"meaning",
|
|
172
|
+
"processed is cumulative across calls, not current context",
|
|
173
|
+
"dim",
|
|
174
|
+
),
|
|
175
|
+
]
|
|
176
|
+
cache_efficiency = totals.get("cache_efficiency")
|
|
177
|
+
if isinstance(cache_efficiency, dict) and cache_efficiency.get("hit_ratio") is not None:
|
|
178
|
+
largest = cache_efficiency.get("largest_uncached_call")
|
|
179
|
+
largest_text = ""
|
|
180
|
+
if isinstance(largest, dict):
|
|
181
|
+
largest_text = (
|
|
182
|
+
f"; max {_format_exact_token_count(largest.get('uncached_prompt_tokens'))} "
|
|
183
|
+
f"at call {int(largest.get('call_position') or 0)}"
|
|
184
|
+
)
|
|
185
|
+
total_rows.append(
|
|
186
|
+
(
|
|
187
|
+
"cache hit",
|
|
188
|
+
f"{float(cache_efficiency['hit_ratio']) * 100:.2f}%{largest_text}",
|
|
189
|
+
"plain",
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
if unknown_total > 0:
|
|
193
|
+
total_rows.append(
|
|
194
|
+
("note", f"{unknown_total} call(s) have unknown cost (missing pricing)", "warn")
|
|
195
|
+
)
|
|
196
|
+
cache_cost_unknown = int(totals.get("cache_cost_pricing_missing_calls") or 0)
|
|
197
|
+
if cache_cost_unknown > 0:
|
|
198
|
+
total_rows.append(
|
|
199
|
+
(
|
|
200
|
+
"note",
|
|
201
|
+
f"{cache_cost_unknown} cached call(s) need provider-specific cache pricing",
|
|
202
|
+
"warn",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
corrected_total = int(totals.get("corrected_usage_calls") or 0)
|
|
206
|
+
if corrected_total > 0:
|
|
207
|
+
total_rows.append(
|
|
208
|
+
("note", f"{corrected_total} provider usage record(s) corrected before display", "warn")
|
|
209
|
+
)
|
|
210
|
+
request_estimate = totals.get("request_token_estimate")
|
|
211
|
+
if isinstance(request_estimate, dict):
|
|
212
|
+
schema_over_calls = int(request_estimate.get("tool_schema_budget_exceeded_calls") or 0)
|
|
213
|
+
if schema_over_calls > 0:
|
|
214
|
+
schema_overage = int(request_estimate.get("tool_schema_budget_overage_tokens") or 0)
|
|
215
|
+
largest_tool = int(request_estimate.get("tool_schema_largest_tool_tokens") or 0)
|
|
216
|
+
total_rows.append(
|
|
217
|
+
(
|
|
218
|
+
"note",
|
|
219
|
+
(
|
|
220
|
+
f"{schema_over_calls} call(s) exceeded tool schema shadow budget "
|
|
221
|
+
f"(+{_format_exact_token_count(schema_overage)}; "
|
|
222
|
+
f"largest tool {_format_exact_token_count(largest_tool)})"
|
|
223
|
+
),
|
|
224
|
+
"warn",
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
return {
|
|
228
|
+
"title": "Usage",
|
|
229
|
+
"sections": [("Per model", model_rows), ("Total", total_rows)],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# --------------------------------------------------------------- /context (/ctx alias)
|
|
234
|
+
def _chat_context_panel_spec(*, session: Any) -> PanelSpec:
|
|
235
|
+
"""Context-window usage as a panel (mirrors the primary ``/context`` table)."""
|
|
236
|
+
if not callable(getattr(session, "context_left", None)):
|
|
237
|
+
return {
|
|
238
|
+
"title": "Context Window",
|
|
239
|
+
"sections": [
|
|
240
|
+
("Context", [("status", "Context tracking unavailable for this session.", "plain")])
|
|
241
|
+
],
|
|
242
|
+
}
|
|
243
|
+
_refresh_chat_hud_context_cache(session)
|
|
244
|
+
ctx = getattr(session, "_hud_context_cache", None)
|
|
245
|
+
if ctx is None:
|
|
246
|
+
return {
|
|
247
|
+
"title": "Context Window",
|
|
248
|
+
"sections": [
|
|
249
|
+
("Context", [("status", "Context tracking unavailable for this session.", "plain")])
|
|
250
|
+
],
|
|
251
|
+
}
|
|
252
|
+
context_window = getattr(ctx, "context_window_tokens", getattr(ctx, "max_input_tokens", "n/a"))
|
|
253
|
+
remaining = getattr(
|
|
254
|
+
ctx, "context_window_remaining_tokens", getattr(ctx, "remaining_tokens", "n/a")
|
|
255
|
+
)
|
|
256
|
+
percent = getattr(ctx, "context_window_percent_left", getattr(ctx, "percent_left", None))
|
|
257
|
+
used = getattr(ctx, "used_input_tokens", "n/a")
|
|
258
|
+
effective_budget = getattr(ctx, "effective_input_budget", None)
|
|
259
|
+
effective_remaining = getattr(ctx, "effective_remaining_tokens", None)
|
|
260
|
+
effective_percent = getattr(ctx, "effective_percent_left", None)
|
|
261
|
+
startup_baseline = getattr(ctx, "startup_baseline_tokens", None)
|
|
262
|
+
dynamic_budget = getattr(ctx, "dynamic_context_budget_tokens", None)
|
|
263
|
+
dynamic_used = getattr(ctx, "dynamic_context_used_tokens", None)
|
|
264
|
+
dynamic_remaining = getattr(ctx, "dynamic_context_remaining_tokens", None)
|
|
265
|
+
dynamic_percent = getattr(ctx, "dynamic_context_percent_left", None)
|
|
266
|
+
|
|
267
|
+
# Colour the percent from the SAME metric we display (the context-window
|
|
268
|
+
# percent), not the dynamic-budget percent — otherwise a healthy 60%-left
|
|
269
|
+
# window could be painted red because the baseline-subtracted dynamic value
|
|
270
|
+
# is much lower. Fall back to the session helper only when the window value
|
|
271
|
+
# is missing.
|
|
272
|
+
displayed_percent = percent
|
|
273
|
+
if displayed_percent is None:
|
|
274
|
+
displayed_percent = _chat_context_percent_value(session)
|
|
275
|
+
try:
|
|
276
|
+
percent_float = float(displayed_percent) if displayed_percent is not None else None
|
|
277
|
+
except (TypeError, ValueError):
|
|
278
|
+
percent_float = None
|
|
279
|
+
if percent_float is None:
|
|
280
|
+
pct_tone = "plain"
|
|
281
|
+
elif percent_float < 10.0:
|
|
282
|
+
pct_tone = "err"
|
|
283
|
+
elif percent_float < 25.0:
|
|
284
|
+
pct_tone = "warn"
|
|
285
|
+
else:
|
|
286
|
+
pct_tone = "accent"
|
|
287
|
+
percent_display = _format_chat_context_percent(displayed_percent)
|
|
288
|
+
|
|
289
|
+
context_rows: list[tuple[str, str, str]] = [
|
|
290
|
+
("model", str(getattr(ctx, "model_name", "-")), "plain"),
|
|
291
|
+
("metadata source", str(getattr(ctx, "source", "-")), "dim"),
|
|
292
|
+
(
|
|
293
|
+
"capacity route",
|
|
294
|
+
str(getattr(ctx, "capacity_provider_key", None) or "-"),
|
|
295
|
+
"dim",
|
|
296
|
+
),
|
|
297
|
+
("token source", str(getattr(ctx, "token_count_source", "-")), "dim"),
|
|
298
|
+
("confidence", str(getattr(ctx, "token_count_confidence", "-")), "dim"),
|
|
299
|
+
("context window", str(context_window), "plain"),
|
|
300
|
+
("used (projected)", str(used), "plain"),
|
|
301
|
+
("left tokens", str(remaining), "plain"),
|
|
302
|
+
("left %", percent_display, pct_tone),
|
|
303
|
+
]
|
|
304
|
+
anchor_source = getattr(ctx, "anchor_token_count_source", None)
|
|
305
|
+
if anchor_source:
|
|
306
|
+
context_rows.insert(
|
|
307
|
+
4,
|
|
308
|
+
("request anchor", str(anchor_source), "dim"),
|
|
309
|
+
)
|
|
310
|
+
budget_rows: list[tuple[str, str, str]] = [
|
|
311
|
+
("input budget", _context_metric_display(effective_budget), "plain"),
|
|
312
|
+
("budget left", _context_metric_display(effective_remaining), "plain"),
|
|
313
|
+
(
|
|
314
|
+
"budget left %",
|
|
315
|
+
_format_chat_context_percent(_context_float(effective_percent)),
|
|
316
|
+
_context_percent_tone(effective_percent),
|
|
317
|
+
),
|
|
318
|
+
]
|
|
319
|
+
conversation_rows: list[tuple[str, str, str]] = [
|
|
320
|
+
("startup baseline", _context_metric_display(startup_baseline), "plain"),
|
|
321
|
+
("conversation budget", _context_metric_display(dynamic_budget), "plain"),
|
|
322
|
+
("conversation used", _context_metric_display(dynamic_used), "plain"),
|
|
323
|
+
("conversation left", _context_metric_display(dynamic_remaining), "plain"),
|
|
324
|
+
(
|
|
325
|
+
"conversation left %",
|
|
326
|
+
_format_chat_context_percent(_context_float(dynamic_percent)),
|
|
327
|
+
_context_percent_tone(dynamic_percent),
|
|
328
|
+
),
|
|
329
|
+
]
|
|
330
|
+
sections: list[tuple[str, list[tuple[str, str, str]]]] = [("Context", context_rows)]
|
|
331
|
+
if any(row[1] != "n/a" for row in budget_rows):
|
|
332
|
+
sections.append(("Effective Input Budget", budget_rows))
|
|
333
|
+
if any(row[1] != "n/a" for row in conversation_rows):
|
|
334
|
+
sections.append(("Conversation Context", conversation_rows))
|
|
335
|
+
return {
|
|
336
|
+
"title": "Context Window",
|
|
337
|
+
"sections": sections,
|
|
338
|
+
"hint": "/compact to reduce context · Esc close",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _context_float(value: Any) -> float | None:
|
|
343
|
+
if value is None:
|
|
344
|
+
return None
|
|
345
|
+
try:
|
|
346
|
+
return float(value)
|
|
347
|
+
except (TypeError, ValueError):
|
|
348
|
+
return None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _context_percent_tone(value: Any) -> str:
|
|
352
|
+
percent = _context_float(value)
|
|
353
|
+
if percent is None:
|
|
354
|
+
return "plain"
|
|
355
|
+
if percent < 10.0:
|
|
356
|
+
return "err"
|
|
357
|
+
if percent < 25.0:
|
|
358
|
+
return "warn"
|
|
359
|
+
return "accent"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _context_metric_display(value: Any) -> str:
|
|
363
|
+
if value is None:
|
|
364
|
+
return "n/a"
|
|
365
|
+
try:
|
|
366
|
+
parsed = int(value)
|
|
367
|
+
except (TypeError, ValueError):
|
|
368
|
+
return str(value)
|
|
369
|
+
return str(max(0, parsed))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# ----------------------------------------------------------------- /model-info
|
|
373
|
+
def _chat_model_info_panel_spec(*, session: Any, model_ref: str = "") -> PanelSpec:
|
|
374
|
+
"""Resolved model metadata as a panel (mirrors ``/model-info``)."""
|
|
375
|
+
model_name = (model_ref or "").strip() or str(
|
|
376
|
+
getattr(getattr(session, "client", None), "model", "") or ""
|
|
377
|
+
).strip()
|
|
378
|
+
if not model_name:
|
|
379
|
+
return {
|
|
380
|
+
"title": "Model Info",
|
|
381
|
+
"sections": [
|
|
382
|
+
("Model", [("status", "Model info unavailable (missing model name).", "plain")])
|
|
383
|
+
],
|
|
384
|
+
}
|
|
385
|
+
registry = getattr(session, "model_registry", None)
|
|
386
|
+
if registry is None:
|
|
387
|
+
return {
|
|
388
|
+
"title": "Model Info",
|
|
389
|
+
"sections": [
|
|
390
|
+
("Model", [("status", "Model info unavailable for this session.", "plain")])
|
|
391
|
+
],
|
|
392
|
+
}
|
|
393
|
+
try:
|
|
394
|
+
meta = registry.get(model_name)
|
|
395
|
+
except Exception as exc: # noqa: BLE001 - surface a resolve failure inline
|
|
396
|
+
return {
|
|
397
|
+
"title": f"Model Info ({model_name})",
|
|
398
|
+
"sections": [("Model", [("error", str(exc), "err")])],
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
field_sources = getattr(meta, "field_sources", {}) or {}
|
|
402
|
+
|
|
403
|
+
def _src(key: str) -> str:
|
|
404
|
+
return str(field_sources.get(key, "unknown"))
|
|
405
|
+
|
|
406
|
+
rows: list[tuple[str, str, str]] = [
|
|
407
|
+
("resolved", str(getattr(meta, "model_name", model_name)), "accent"),
|
|
408
|
+
("source", str(getattr(meta, "source", "unknown")), "plain"),
|
|
409
|
+
(
|
|
410
|
+
"context window",
|
|
411
|
+
f"{getattr(meta, 'context_window_tokens', '-')} ({_src('context_window_tokens')})",
|
|
412
|
+
"plain",
|
|
413
|
+
),
|
|
414
|
+
(
|
|
415
|
+
"max output",
|
|
416
|
+
f"{getattr(meta, 'max_output_tokens', '-')} ({_src('max_output_tokens')})",
|
|
417
|
+
"plain",
|
|
418
|
+
),
|
|
419
|
+
(
|
|
420
|
+
"vision",
|
|
421
|
+
f"{getattr(meta, 'supports_vision', '-')} ({_src('supports_vision')})",
|
|
422
|
+
"plain",
|
|
423
|
+
),
|
|
424
|
+
(
|
|
425
|
+
"input $/tok",
|
|
426
|
+
f"{getattr(meta, 'input_cost_per_token', '-')} ({_src('input_cost_per_token')})",
|
|
427
|
+
"plain",
|
|
428
|
+
),
|
|
429
|
+
(
|
|
430
|
+
"output $/tok",
|
|
431
|
+
f"{getattr(meta, 'output_cost_per_token', '-')} ({_src('output_cost_per_token')})",
|
|
432
|
+
"plain",
|
|
433
|
+
),
|
|
434
|
+
]
|
|
435
|
+
last_error = getattr(registry, "last_error", None)
|
|
436
|
+
if last_error:
|
|
437
|
+
rows.append(("registry error", str(last_error), "err"))
|
|
438
|
+
warnings = "; ".join(getattr(meta, "warnings", ())[:5])
|
|
439
|
+
if warnings:
|
|
440
|
+
rows.append(("warnings", warnings, "warn"))
|
|
441
|
+
return {"title": f"Model Info ({model_name})", "sections": [("Model", rows)]}
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# -------------------------------------------------------------------- /config
|
|
445
|
+
def _chat_config_panel_spec(*, session: Any) -> PanelSpec:
|
|
446
|
+
"""Tracked-model config as a panel (mirrors ``_chat_config_panel``).
|
|
447
|
+
|
|
448
|
+
The interactive config menu cannot run inside the alt-screen, so the bare
|
|
449
|
+
``/config`` shows this read-only panel; ``/config set|clear`` still apply via
|
|
450
|
+
the command runner.
|
|
451
|
+
"""
|
|
452
|
+
from .chat_status import _chat_used_models # self-contained getter
|
|
453
|
+
|
|
454
|
+
models = _chat_used_models(session)
|
|
455
|
+
registry = getattr(session, "model_registry", None)
|
|
456
|
+
|
|
457
|
+
model_rows: list[tuple[str, str, str]] = []
|
|
458
|
+
if models:
|
|
459
|
+
for idx, name in enumerate(models, start=1):
|
|
460
|
+
summary = ""
|
|
461
|
+
if registry is not None:
|
|
462
|
+
try:
|
|
463
|
+
meta = registry.get(name)
|
|
464
|
+
except Exception as exc: # noqa: BLE001
|
|
465
|
+
summary = f"error={exc}"
|
|
466
|
+
else:
|
|
467
|
+
field_sources = getattr(meta, "field_sources", {}) or {}
|
|
468
|
+
src = field_sources.get(
|
|
469
|
+
"context_window_tokens", getattr(meta, "source", "unknown")
|
|
470
|
+
)
|
|
471
|
+
summary = (
|
|
472
|
+
f"ctx={getattr(meta, 'context_window_tokens', '-')} "
|
|
473
|
+
f"out={getattr(meta, 'max_output_tokens', '-')} src={src}"
|
|
474
|
+
)
|
|
475
|
+
value = f"{name} {summary}".rstrip() if summary else str(name)
|
|
476
|
+
model_rows.append((str(idx), value, "accent" if idx == 1 else "plain"))
|
|
477
|
+
else:
|
|
478
|
+
model_rows.append(("models", "No tracked models in this session yet.", "plain"))
|
|
479
|
+
|
|
480
|
+
usage_rows: list[tuple[str, str, str]] = [
|
|
481
|
+
("set", "/config set <model|index> <ctx> <max_out> [vision] [in$] [out$]", "dim"),
|
|
482
|
+
("clear", "/config clear <model|index>", "dim"),
|
|
483
|
+
("example", "/config set 1 128000 4096 false", "dim"),
|
|
484
|
+
]
|
|
485
|
+
return {
|
|
486
|
+
"title": "Model Config",
|
|
487
|
+
"sections": [("Tracked models", model_rows), ("Manage", usage_rows)],
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# ------------------------------------------------------------------- /toolbar
|
|
492
|
+
def _chat_toolbar_panel_spec(*, session: Any) -> PanelSpec:
|
|
493
|
+
"""Status-toolbar items as a panel (mirrors bare ``/toolbar``)."""
|
|
494
|
+
from .cli_common import (
|
|
495
|
+
_CHAT_TOOLBAR_ITEM_ORDER,
|
|
496
|
+
_DEFAULT_TOOLBAR_ITEMS,
|
|
497
|
+
_VALID_TOOLBAR_ITEMS,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
cfg_obj = getattr(session, "cfg", None)
|
|
501
|
+
raw_items = getattr(cfg_obj, "toolbar_items", None) if cfg_obj is not None else None
|
|
502
|
+
active: list[str] = []
|
|
503
|
+
seen: set[str] = set()
|
|
504
|
+
for raw_item in list(raw_items or list(_DEFAULT_TOOLBAR_ITEMS)):
|
|
505
|
+
item = str(raw_item).strip().lower()
|
|
506
|
+
if not item or item in seen or item not in _VALID_TOOLBAR_ITEMS:
|
|
507
|
+
continue
|
|
508
|
+
seen.add(item)
|
|
509
|
+
active.append(item)
|
|
510
|
+
available = [item for item in _CHAT_TOOLBAR_ITEM_ORDER if item not in set(active)]
|
|
511
|
+
|
|
512
|
+
item_rows: list[tuple[str, str, str]] = [
|
|
513
|
+
("active", ", ".join(active) if active else "(none)", "accent"),
|
|
514
|
+
("available", ", ".join(available) if available else "(none)", "plain"),
|
|
515
|
+
]
|
|
516
|
+
manage_rows: list[tuple[str, str, str]] = [
|
|
517
|
+
("add", "/toolbar add <item>", "dim"),
|
|
518
|
+
("remove", "/toolbar remove <item>", "dim"),
|
|
519
|
+
("reset", "/toolbar reset", "dim"),
|
|
520
|
+
("save", "/toolbar save", "dim"),
|
|
521
|
+
]
|
|
522
|
+
return {
|
|
523
|
+
"title": "Toolbar Items",
|
|
524
|
+
"sections": [("Items", item_rows), ("Manage", manage_rows)],
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# ----------------------------------------------------------------- /terminals
|
|
529
|
+
def _chat_terminals_panel_spec(*, session: Any) -> PanelSpec:
|
|
530
|
+
"""Background processes as a panel (mirrors ``/terminals list``)."""
|
|
531
|
+
terminal_manager = getattr(session, "terminal_manager", None)
|
|
532
|
+
if terminal_manager is None:
|
|
533
|
+
return {
|
|
534
|
+
"title": "Background Terminals",
|
|
535
|
+
"sections": [
|
|
536
|
+
(
|
|
537
|
+
"Terminals",
|
|
538
|
+
[("status", "Background terminals are unavailable in this session.", "plain")],
|
|
539
|
+
)
|
|
540
|
+
],
|
|
541
|
+
}
|
|
542
|
+
try:
|
|
543
|
+
summaries = terminal_manager.list()
|
|
544
|
+
except Exception as exc: # noqa: BLE001
|
|
545
|
+
return {
|
|
546
|
+
"title": "Background Terminals",
|
|
547
|
+
"sections": [("Terminals", [("error", str(exc), "err")])],
|
|
548
|
+
}
|
|
549
|
+
if not summaries:
|
|
550
|
+
process_rows: list[tuple[str, str, str]] = [("status", "No background processes.", "plain")]
|
|
551
|
+
else:
|
|
552
|
+
process_rows = []
|
|
553
|
+
for summary in summaries:
|
|
554
|
+
status = str(getattr(summary, "status", "-"))
|
|
555
|
+
status_lc = status.casefold()
|
|
556
|
+
if status_lc in {"failed", "error"}:
|
|
557
|
+
tone = "err"
|
|
558
|
+
elif status_lc == "running":
|
|
559
|
+
tone = "accent"
|
|
560
|
+
else:
|
|
561
|
+
tone = "plain"
|
|
562
|
+
exit_code = getattr(summary, "exit_code", None)
|
|
563
|
+
runtime = _format_terminal_runtime_s(float(getattr(summary, "runtime_s", 0) or 0))
|
|
564
|
+
value = (
|
|
565
|
+
f"{status} · exit {('-' if exit_code is None else exit_code)} "
|
|
566
|
+
f"· {runtime} · {getattr(summary, 'cmd', '')}"
|
|
567
|
+
)
|
|
568
|
+
process_rows.append((str(getattr(summary, "process_id", "-")), value, tone))
|
|
569
|
+
manage_rows: list[tuple[str, str, str]] = [
|
|
570
|
+
("show", "/terminals show <id>", "dim"),
|
|
571
|
+
("kill", "/terminals kill <id>", "dim"),
|
|
572
|
+
]
|
|
573
|
+
return {
|
|
574
|
+
"title": "Background Terminals",
|
|
575
|
+
"sections": [("Processes", process_rows), ("Manage", manage_rows)],
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# --------------------------------------------------------------------- /skill
|
|
580
|
+
def _chat_skill_listing_panel_spec(*, session: Any) -> PanelSpec:
|
|
581
|
+
"""Discovered skills as a panel (mirrors bare ``/skill``)."""
|
|
582
|
+
enabled, ordered, issues = _session_skill_listing(session)
|
|
583
|
+
if not enabled:
|
|
584
|
+
return {
|
|
585
|
+
"title": "Skills",
|
|
586
|
+
"sections": [
|
|
587
|
+
("Skills", [("status", "Skills are disabled for this session config.", "plain")])
|
|
588
|
+
],
|
|
589
|
+
}
|
|
590
|
+
if not ordered:
|
|
591
|
+
return {
|
|
592
|
+
"title": "Skills",
|
|
593
|
+
"sections": [
|
|
594
|
+
(
|
|
595
|
+
"Skills",
|
|
596
|
+
[("status", "No skills discovered in the supported skill roots.", "plain")],
|
|
597
|
+
)
|
|
598
|
+
],
|
|
599
|
+
}
|
|
600
|
+
skill_rows = [
|
|
601
|
+
(str(getattr(skill, "name", "")), str(getattr(skill, "description", "") or ""), "plain")
|
|
602
|
+
for skill in ordered
|
|
603
|
+
]
|
|
604
|
+
sections: list[tuple[str, list[tuple[str, str, str]]]] = [
|
|
605
|
+
(f"Skills ({len(ordered)})", skill_rows)
|
|
606
|
+
]
|
|
607
|
+
issue_rows: list[tuple[str, str, str]] = []
|
|
608
|
+
for issue in issues:
|
|
609
|
+
source_path = getattr(issue, "source_path", None)
|
|
610
|
+
message = str(getattr(issue, "message", "") or "").strip()
|
|
611
|
+
if source_path is not None and message:
|
|
612
|
+
issue_rows.append((str(source_path), message, "warn"))
|
|
613
|
+
if issue_rows:
|
|
614
|
+
sections.append(("Skipped", issue_rows))
|
|
615
|
+
return {
|
|
616
|
+
"title": "Skills",
|
|
617
|
+
"sections": sections,
|
|
618
|
+
"hint": "/skill <name> for info · /skill <name> <task> to attach · Esc close",
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ------------------------------------------------------- subagent descriptions
|
|
623
|
+
# Leading boilerplate stripped from a subagent description before summarising
|
|
624
|
+
# (longest-first so the most specific prefix wins). The blurbs all open with some
|
|
625
|
+
# variant of "Use this when you need to …" / "Catch-all subagent for …", which
|
|
626
|
+
# carries no information for a one-line picker label.
|
|
627
|
+
_SUBAGENT_DESC_BOILERPLATE = (
|
|
628
|
+
"use this agent when you need to ",
|
|
629
|
+
"use this agent when you need ",
|
|
630
|
+
"use this agent when ",
|
|
631
|
+
"use this when you need to ",
|
|
632
|
+
"use this when you need an ",
|
|
633
|
+
"use this when you need a ",
|
|
634
|
+
"use this when you need ",
|
|
635
|
+
"use this when answering means ",
|
|
636
|
+
"use this when ",
|
|
637
|
+
"use this for ",
|
|
638
|
+
"use for ",
|
|
639
|
+
"catch-all subagent for ",
|
|
640
|
+
"catch-all agent for ",
|
|
641
|
+
"catch-all for ",
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _short_subagent_desc(text: str, limit: int = 46) -> str:
|
|
646
|
+
"""Condense a (potentially huge) subagent description to a short, scannable
|
|
647
|
+
summary shown to the right of the option in the spawn picker.
|
|
648
|
+
|
|
649
|
+
Collapses whitespace, strips the boilerplate "Use this when you need to …"
|
|
650
|
+
lead-in, keeps only the first clause (up to the first ``:`` / ``.`` / dash),
|
|
651
|
+
capitalises it, and hard-truncates with an ellipsis so each option stays a
|
|
652
|
+
single readable line.
|
|
653
|
+
"""
|
|
654
|
+
compact = " ".join(str(text or "").split())
|
|
655
|
+
if not compact:
|
|
656
|
+
return ""
|
|
657
|
+
lowered = compact.lower()
|
|
658
|
+
for prefix in _SUBAGENT_DESC_BOILERPLATE:
|
|
659
|
+
if lowered.startswith(prefix):
|
|
660
|
+
compact = compact[len(prefix) :]
|
|
661
|
+
break
|
|
662
|
+
# Keep only the first clause/sentence — the crisp "what it does".
|
|
663
|
+
cut = len(compact)
|
|
664
|
+
for sep in (": ", ". ", "; ", " — ", " - "):
|
|
665
|
+
idx = compact.find(sep)
|
|
666
|
+
if idx != -1:
|
|
667
|
+
cut = min(cut, idx)
|
|
668
|
+
summary = compact[:cut].rstrip(" ,;:.—-")
|
|
669
|
+
if summary:
|
|
670
|
+
summary = summary[0].upper() + summary[1:]
|
|
671
|
+
if len(summary) > limit:
|
|
672
|
+
head = summary[: max(1, limit - 1)]
|
|
673
|
+
# Prefer breaking on a word boundary so we never clip mid-word ("recent c…").
|
|
674
|
+
space = head.rfind(" ")
|
|
675
|
+
if space >= limit // 2:
|
|
676
|
+
head = head[:space]
|
|
677
|
+
summary = head.rstrip(" ,;:.—-") + "…"
|
|
678
|
+
return summary
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
# --------------------------------------------------------------------- Forge
|
|
682
|
+
# The status→bucket authority lives in ``...tui.forge_status`` so the live swarm
|
|
683
|
+
# table, the ``/show`` plan panel here, and the end-of-run summary can never
|
|
684
|
+
# disagree about whether a task is done, failed, or still running.
|
|
685
|
+
def _forge_status_tone(canonical: str) -> str:
|
|
686
|
+
"""Map a canonical task status to a panel value tone (accent/err/dim/plain).
|
|
687
|
+
|
|
688
|
+
done → accent (green), failed → err (red), obsolete → dim, everything else
|
|
689
|
+
(running/planned) → plain — the SAME semantic colours the live swarm table
|
|
690
|
+
uses, so a task reads identically in ``/show`` and while it runs."""
|
|
691
|
+
bucket = forge_status_bucket(canonical)
|
|
692
|
+
if bucket == "done":
|
|
693
|
+
return "accent"
|
|
694
|
+
if bucket == "failed":
|
|
695
|
+
return "err"
|
|
696
|
+
if bucket == "obsolete":
|
|
697
|
+
return "dim"
|
|
698
|
+
return "plain"
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
# The "how Forge works" guidance shown in the centered popup when the user runs a
|
|
702
|
+
# plain ``/forge``. Markdown so the doc panel renders headings/lists; the closing
|
|
703
|
+
# call-to-action pairs with the panel's "Enter to begin · Esc to cancel" hint.
|
|
704
|
+
_FORGE_INTRO_BODY = """\
|
|
705
|
+
# Forge — autonomous build mode
|
|
706
|
+
|
|
707
|
+
Forge turns a goal into a reviewed plan, then runs a **swarm of agents** to build
|
|
708
|
+
it task by task. You stay in control — nothing is built until you say so.
|
|
709
|
+
|
|
710
|
+
## How it works
|
|
711
|
+
|
|
712
|
+
1. **Set the goal** — describe what you want built (just type it, or `/goal`).
|
|
713
|
+
2. **Shape the plan** — add or refine tasks with `/task`, or let the planner
|
|
714
|
+
assistant draft them for you.
|
|
715
|
+
3. **Review** — inspect the plan and task table with `/show` and `/plan`.
|
|
716
|
+
4. **Execute** — `/execute plan` runs the swarm to build every task, live.
|
|
717
|
+
5. **Finish** — `/done` (or `/back`) returns you to normal chat.
|
|
718
|
+
|
|
719
|
+
## Handy commands
|
|
720
|
+
|
|
721
|
+
- `/show` — plan summary: goal, requirements, tasks
|
|
722
|
+
- `/plan` — view or edit the plan (tasks · markdown · edit)
|
|
723
|
+
- `/assets` — attach and review reference files
|
|
724
|
+
- `/execute plan` — run the build
|
|
725
|
+
- `/done` · `/back` — leave Forge
|
|
726
|
+
|
|
727
|
+
Press **Enter** to choose your planner and begin, or **Esc** to stay in chat."""
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def _chat_forge_intro_panel_spec() -> PanelSpec:
|
|
731
|
+
"""Guidance popup shown for a plain ``/forge`` before entering the session.
|
|
732
|
+
|
|
733
|
+
Returns a document-panel spec (rendered through the TUI markdown pipeline)
|
|
734
|
+
carrying a ``confirm`` command: the centered popup explains how Forge works
|
|
735
|
+
and, on Enter, routes ``/forge`` to the command runner to actually enter the
|
|
736
|
+
planning session; Esc cancels. ``/forge resume`` and re-entry while already in
|
|
737
|
+
Forge skip this intro (handled by the caller's gating).
|
|
738
|
+
"""
|
|
739
|
+
return {
|
|
740
|
+
"title": "Forge",
|
|
741
|
+
"body": _FORGE_INTRO_BODY,
|
|
742
|
+
"hint": "↵ Enter to begin · Esc to cancel",
|
|
743
|
+
"confirm": "/forge",
|
|
744
|
+
"accent": "forge",
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _chat_forge_plan_panel_spec(*, paths: Any, plan: dict[str, Any]) -> PanelSpec:
|
|
749
|
+
"""Forge plan summary as a panel — mirrors ``_show_forge_plan_summary``.
|
|
750
|
+
|
|
751
|
+
Renders the goal/summary + per-task table that the classic ``/show`` and
|
|
752
|
+
``/plan tasks`` print, as a centered popup instead of a flat Rich table dump.
|
|
753
|
+
Used in Forge mode for ``/show`` and ``/plan tasks|table|view``.
|
|
754
|
+
"""
|
|
755
|
+
from ...forge import requirement_text
|
|
756
|
+
from ...swarm_scheduler import canonical_task_status
|
|
757
|
+
from .cli_common import _forge_task_mcp_summary_label, _forge_task_status_counts
|
|
758
|
+
from .forge_asset_view import forge_asset_view_count
|
|
759
|
+
|
|
760
|
+
goal = str(plan.get("project_goal") or "").strip() or "(not set)"
|
|
761
|
+
summary = str(plan.get("summary") or "").strip() or "(not set)"
|
|
762
|
+
tasks_obj = plan.get("tasks") or []
|
|
763
|
+
tasks = tasks_obj if isinstance(tasks_obj, list) else []
|
|
764
|
+
task_count = len(tasks)
|
|
765
|
+
try:
|
|
766
|
+
asset_count = forge_asset_view_count(paths, plan)
|
|
767
|
+
except Exception: # noqa: BLE001 - never crash the UI on a panel
|
|
768
|
+
asset_count = 0
|
|
769
|
+
try:
|
|
770
|
+
done, failed, remaining = _forge_task_status_counts(plan)
|
|
771
|
+
except Exception: # noqa: BLE001
|
|
772
|
+
done = failed = remaining = 0
|
|
773
|
+
|
|
774
|
+
overview: list[tuple[str, str, str]] = [
|
|
775
|
+
("run", str(getattr(paths, "run_id", "-")), "plain"),
|
|
776
|
+
("goal", goal, "plain"),
|
|
777
|
+
("summary", summary, "plain"),
|
|
778
|
+
(
|
|
779
|
+
"tasks",
|
|
780
|
+
f"{task_count} total · {done} done · {failed} failed · {remaining} remaining",
|
|
781
|
+
"accent" if (task_count and failed == 0) else "plain",
|
|
782
|
+
),
|
|
783
|
+
("assets", str(asset_count), "plain"),
|
|
784
|
+
]
|
|
785
|
+
sections: list[tuple[str, list[tuple[str, str, str]]]] = [("Plan", overview)]
|
|
786
|
+
|
|
787
|
+
requirements_obj = plan.get("requirements") or []
|
|
788
|
+
requirements = requirements_obj if isinstance(requirements_obj, list) else []
|
|
789
|
+
if requirements:
|
|
790
|
+
req_rows: list[tuple[str, str, str]] = []
|
|
791
|
+
for idx, req in enumerate(requirements[:12], start=1):
|
|
792
|
+
text = " ".join(str(requirement_text(req) or "").split())
|
|
793
|
+
if not text:
|
|
794
|
+
continue
|
|
795
|
+
req_rows.append((str(idx), text, "plain"))
|
|
796
|
+
if len(requirements) > 12:
|
|
797
|
+
req_rows.append(("…", f"({len(requirements) - 12} more)", "dim"))
|
|
798
|
+
if req_rows:
|
|
799
|
+
sections.append((f"Requirements ({len(requirements)})", req_rows))
|
|
800
|
+
|
|
801
|
+
if tasks:
|
|
802
|
+
task_rows: list[tuple[str, str, str]] = []
|
|
803
|
+
for task in tasks:
|
|
804
|
+
if not isinstance(task, dict):
|
|
805
|
+
continue
|
|
806
|
+
tid = str(task.get("id") or "-")
|
|
807
|
+
raw_status = str(task.get("status") or "planned")
|
|
808
|
+
canonical = canonical_task_status(raw_status)
|
|
809
|
+
tone = _forge_status_tone(canonical)
|
|
810
|
+
# Prefix the SAME glyph the live swarm table uses (✓/✗/○/·) so a task
|
|
811
|
+
# looks identical here and while it executes.
|
|
812
|
+
glyph = forge_status_glyph(canonical)
|
|
813
|
+
title = str(task.get("title") or "-")
|
|
814
|
+
value = f"{glyph} {raw_status} · {title}"
|
|
815
|
+
extra: list[str] = []
|
|
816
|
+
deps = task.get("dependencies") or []
|
|
817
|
+
if isinstance(deps, list) and deps:
|
|
818
|
+
extra.append("deps: " + ", ".join(str(dep) for dep in deps))
|
|
819
|
+
try:
|
|
820
|
+
mcp = _forge_task_mcp_summary_label(task)
|
|
821
|
+
except Exception: # noqa: BLE001
|
|
822
|
+
mcp = "off"
|
|
823
|
+
if mcp and mcp != "off":
|
|
824
|
+
extra.append(f"mcp: {mcp}")
|
|
825
|
+
if extra:
|
|
826
|
+
value += " · " + " · ".join(extra)
|
|
827
|
+
task_rows.append((tid, value, tone))
|
|
828
|
+
sections.append((f"Tasks ({task_count})", task_rows))
|
|
829
|
+
else:
|
|
830
|
+
sections.append(("Tasks", [("-", "(no tasks yet)", "dim")]))
|
|
831
|
+
|
|
832
|
+
return {
|
|
833
|
+
"title": f"Forge Plan · {getattr(paths, 'run_id', '')}".rstrip(" ·"),
|
|
834
|
+
"sections": sections,
|
|
835
|
+
"hint": "/execute plan to run · /plan markdown for PLAN.md · Esc close",
|
|
836
|
+
"accent": "forge",
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _chat_forge_markdown_panel_spec(*, paths: Any, plan: dict[str, Any]) -> PanelSpec:
|
|
841
|
+
"""PLAN.md preview as a scrollable doc panel — replaces the classic pager.
|
|
842
|
+
|
|
843
|
+
Saves the in-memory plan first (so PLAN.md reflects edits, like the classic
|
|
844
|
+
``_show_forge_plan_markdown``), reads PLAN.md, and returns a ``body`` spec the
|
|
845
|
+
TUI renders through its markdown renderer in the centered popup.
|
|
846
|
+
"""
|
|
847
|
+
from ...forge import save_plan
|
|
848
|
+
|
|
849
|
+
try:
|
|
850
|
+
save_plan(paths, plan)
|
|
851
|
+
except Exception: # noqa: BLE001 - still try to show whatever is on disk
|
|
852
|
+
pass
|
|
853
|
+
plan_md_path = getattr(paths, "plan_md_path", None)
|
|
854
|
+
body = ""
|
|
855
|
+
if plan_md_path is not None:
|
|
856
|
+
try:
|
|
857
|
+
body = plan_md_path.read_text(encoding="utf-8")
|
|
858
|
+
except Exception as exc: # noqa: BLE001
|
|
859
|
+
return {
|
|
860
|
+
"title": "PLAN.md",
|
|
861
|
+
"sections": [("PLAN.md", [("error", f"Failed to read PLAN.md: {exc}", "err")])],
|
|
862
|
+
}
|
|
863
|
+
if not body.strip():
|
|
864
|
+
body = "(PLAN.md is empty)"
|
|
865
|
+
return {
|
|
866
|
+
"title": f"PLAN.md · {getattr(paths, 'run_id', '')}".rstrip(" ·"),
|
|
867
|
+
"body": body,
|
|
868
|
+
"hint": "↑↓/PgUp/PgDn scroll · Esc close",
|
|
869
|
+
"accent": "forge",
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
# ---------------------------------------------------------------- Forge assets
|
|
874
|
+
def _format_asset_size(size_bytes: int) -> str:
|
|
875
|
+
"""Human-readable byte size (mirror of assets_modal._format_size)."""
|
|
876
|
+
size = float(size_bytes)
|
|
877
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
878
|
+
if size < 1024 or unit == "GB":
|
|
879
|
+
return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
|
|
880
|
+
size /= 1024
|
|
881
|
+
return f"{size_bytes} B"
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
def _asset_status_tone(status: str) -> str:
|
|
885
|
+
s = str(status or "").strip().lower()
|
|
886
|
+
if s == "ready":
|
|
887
|
+
return "accent"
|
|
888
|
+
if s == "failed":
|
|
889
|
+
return "err"
|
|
890
|
+
if s in {"pending", "running"}:
|
|
891
|
+
return "warn"
|
|
892
|
+
return "plain"
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
def _chat_assets_picker_spec(*, cfg: Any, paths: Any) -> dict[str, Any] | None:
|
|
896
|
+
"""Selectable list of Forge assets — replaces the dead stdin assets modal.
|
|
897
|
+
|
|
898
|
+
Returns ``None`` (so the caller falls through) when the asset surface can't be
|
|
899
|
+
built or there are no assets to pick. Choosing a row submits ``/assets <id>``
|
|
900
|
+
to open the detail panel."""
|
|
901
|
+
from ...assets.surface import build_asset_surface
|
|
902
|
+
from ...forge import load_plan
|
|
903
|
+
|
|
904
|
+
try:
|
|
905
|
+
load_plan(paths)
|
|
906
|
+
except Exception: # noqa: BLE001 - listing still works without a loaded plan
|
|
907
|
+
pass
|
|
908
|
+
try:
|
|
909
|
+
surface = build_asset_surface(cfg=cfg, run_paths=paths)
|
|
910
|
+
entries = surface.list_assets()
|
|
911
|
+
except Exception: # noqa: BLE001
|
|
912
|
+
return None
|
|
913
|
+
if not entries:
|
|
914
|
+
return None
|
|
915
|
+
rows: list[dict[str, Any]] = []
|
|
916
|
+
for entry in entries:
|
|
917
|
+
rec = entry.record
|
|
918
|
+
kind = "img" if str(getattr(rec, "kind", "")) == "image" else "txt"
|
|
919
|
+
size = _format_asset_size(int(getattr(rec, "size_bytes", 0) or 0))
|
|
920
|
+
status = str(getattr(entry, "comprehension_status", "") or "")
|
|
921
|
+
pin = "★ " if getattr(rec, "pinned", False) else ""
|
|
922
|
+
rows.append(
|
|
923
|
+
{
|
|
924
|
+
"label": str(rec.id),
|
|
925
|
+
"description": f"{pin}{rec.title} · {kind} · {status} · {size}",
|
|
926
|
+
"value": str(rec.id),
|
|
927
|
+
"current": False,
|
|
928
|
+
}
|
|
929
|
+
)
|
|
930
|
+
return {
|
|
931
|
+
"title": f"Assets ({len(rows)})",
|
|
932
|
+
"rows": rows,
|
|
933
|
+
"on_select": lambda value: {"submit": f"/assets {value}"},
|
|
934
|
+
"hint": "↑↓ select · Enter to view · Esc cancel",
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _chat_asset_detail_panel_spec(*, cfg: Any, paths: Any, asset_id: str) -> PanelSpec:
|
|
939
|
+
"""One asset's metadata + comprehension summary as a panel (replaces the
|
|
940
|
+
interactive modal's detail view)."""
|
|
941
|
+
from ...assets import AssetError
|
|
942
|
+
from ...assets.surface import build_asset_surface
|
|
943
|
+
|
|
944
|
+
try:
|
|
945
|
+
surface = build_asset_surface(cfg=cfg, run_paths=paths)
|
|
946
|
+
detail = surface.show_asset(asset_id)
|
|
947
|
+
except AssetError as exc:
|
|
948
|
+
return {
|
|
949
|
+
"title": f"Asset · {asset_id}",
|
|
950
|
+
"sections": [("Asset", [("error", str(exc), "err")])],
|
|
951
|
+
}
|
|
952
|
+
except Exception as exc: # noqa: BLE001
|
|
953
|
+
return {"title": "Asset", "sections": [("Asset", [("error", str(exc), "err")])]}
|
|
954
|
+
|
|
955
|
+
rec = detail.record
|
|
956
|
+
deleted = getattr(rec, "deleted_at", None) is not None
|
|
957
|
+
status = "deleted" if deleted else str(getattr(detail, "comprehension_status", "") or "")
|
|
958
|
+
overview: list[tuple[str, str, str]] = [
|
|
959
|
+
("id", str(rec.id), "accent"),
|
|
960
|
+
("title", str(rec.title), "plain"),
|
|
961
|
+
("kind", str(getattr(rec, "kind", "-")), "plain"),
|
|
962
|
+
("size", _format_asset_size(int(getattr(rec, "size_bytes", 0) or 0)), "plain"),
|
|
963
|
+
("mime", str(getattr(rec, "mime", "-")), "plain"),
|
|
964
|
+
(
|
|
965
|
+
"pinned",
|
|
966
|
+
"yes" if getattr(rec, "pinned", False) else "no",
|
|
967
|
+
"accent" if getattr(rec, "pinned", False) else "plain",
|
|
968
|
+
),
|
|
969
|
+
("status", status, "err" if deleted else _asset_status_tone(status)),
|
|
970
|
+
("file", str(getattr(rec, "original_filename", "-")), "dim"),
|
|
971
|
+
]
|
|
972
|
+
sections: list[tuple[str, list[tuple[str, str, str]]]] = [("Asset", overview)]
|
|
973
|
+
description = str(getattr(rec, "description", "") or "").strip()
|
|
974
|
+
if description:
|
|
975
|
+
sections.append(("Description", [("text", description, "plain")]))
|
|
976
|
+
comp = getattr(detail, "comprehension", None)
|
|
977
|
+
if comp is not None:
|
|
978
|
+
data = getattr(comp, "data", None)
|
|
979
|
+
comp_rows: list[tuple[str, str, str]] = [
|
|
980
|
+
("summary", str(getattr(data, "semantic_summary", "") or "-"), "plain"),
|
|
981
|
+
("language", str(getattr(comp, "detected_language", "") or "-"), "dim"),
|
|
982
|
+
("source", str(getattr(comp, "source", "") or "-"), "dim"),
|
|
983
|
+
]
|
|
984
|
+
sections.append(("Comprehension", comp_rows))
|
|
985
|
+
facts = list(getattr(data, "stated_facts", None) or [])
|
|
986
|
+
if facts:
|
|
987
|
+
sections.append(
|
|
988
|
+
(
|
|
989
|
+
"Stated facts",
|
|
990
|
+
[(str(i + 1), str(fact), "plain") for i, fact in enumerate(facts[:8])],
|
|
991
|
+
)
|
|
992
|
+
)
|
|
993
|
+
return {
|
|
994
|
+
"title": f"Asset · {rec.id}",
|
|
995
|
+
"sections": sections,
|
|
996
|
+
"hint": "Esc close",
|
|
997
|
+
"accent": "forge",
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
__all__ = [
|
|
1002
|
+
"_chat_usage_panel_spec",
|
|
1003
|
+
"_chat_context_panel_spec",
|
|
1004
|
+
"_chat_model_info_panel_spec",
|
|
1005
|
+
"_chat_config_panel_spec",
|
|
1006
|
+
"_chat_toolbar_panel_spec",
|
|
1007
|
+
"_chat_terminals_panel_spec",
|
|
1008
|
+
"_chat_skill_listing_panel_spec",
|
|
1009
|
+
"_short_subagent_desc",
|
|
1010
|
+
"_forge_status_tone",
|
|
1011
|
+
"_chat_forge_intro_panel_spec",
|
|
1012
|
+
"_chat_forge_plan_panel_spec",
|
|
1013
|
+
"_chat_forge_markdown_panel_spec",
|
|
1014
|
+
"_format_asset_size",
|
|
1015
|
+
"_asset_status_tone",
|
|
1016
|
+
"_chat_assets_picker_spec",
|
|
1017
|
+
"_chat_asset_detail_panel_spec",
|
|
1018
|
+
]
|