novacode-cli 1.2.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.
- novacode_cli/__init__.py +29 -0
- novacode_cli/__main__.py +6 -0
- novacode_cli/_lazy_heavy.py +99 -0
- novacode_cli/_version.py +3 -0
- novacode_cli/agent_stream.py +178 -0
- novacode_cli/agents/agent_file.py +250 -0
- novacode_cli/agents/agent_mailbox.py +120 -0
- novacode_cli/agents/approval_decisions.py +146 -0
- novacode_cli/agents/arg_repair.py +196 -0
- novacode_cli/agents/async_agents/__init__.py +6 -0
- novacode_cli/agents/async_agents/_config.py +213 -0
- novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
- novacode_cli/agents/async_agents/_specialist.py +84 -0
- novacode_cli/agents/async_agents/_test_commands.py +89 -0
- novacode_cli/agents/async_agents/_tools.py +432 -0
- novacode_cli/agents/async_agents/general_purpose.py +42 -0
- novacode_cli/agents/async_agents/langgraph.json +9 -0
- novacode_cli/agents/async_agents/research.py +22 -0
- novacode_cli/agents/async_context.py +303 -0
- novacode_cli/agents/async_on_demand.py +52 -0
- novacode_cli/agents/async_task_errors.py +75 -0
- novacode_cli/agents/async_workspace.py +27 -0
- novacode_cli/agents/attribution.py +165 -0
- novacode_cli/agents/browser_mode.py +119 -0
- novacode_cli/agents/btw_agent.py +71 -0
- novacode_cli/agents/caveman.py +352 -0
- novacode_cli/agents/caveman_support.py +370 -0
- novacode_cli/agents/code_mode.py +774 -0
- novacode_cli/agents/code_runtime.py +160 -0
- novacode_cli/agents/context_compaction.py +1339 -0
- novacode_cli/agents/core_agent.py +2710 -0
- novacode_cli/agents/default_subagents/__init__.py +24 -0
- novacode_cli/agents/default_subagents/async_subagents.py +296 -0
- novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
- novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
- novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
- novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
- novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
- novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
- novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
- novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
- novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
- novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
- novacode_cli/agents/default_subagents/prompt.py +88 -0
- novacode_cli/agents/default_subagents/subagents.py +138 -0
- novacode_cli/agents/dream_agent.py +93 -0
- novacode_cli/agents/edit_diagnostics.py +362 -0
- novacode_cli/agents/edit_fallback.py +62 -0
- novacode_cli/agents/handoff.py +166 -0
- novacode_cli/agents/invocation.py +152 -0
- novacode_cli/agents/model_budget.py +42 -0
- novacode_cli/agents/model_retry.py +113 -0
- novacode_cli/agents/model_router.py +420 -0
- novacode_cli/agents/openai_decisions.py +186 -0
- novacode_cli/agents/plan_agent/__init__.py +12 -0
- novacode_cli/agents/plan_agent/plan_agent.py +357 -0
- novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
- novacode_cli/agents/plan_execution.py +237 -0
- novacode_cli/agents/ponytail.py +66 -0
- novacode_cli/agents/prime.py +274 -0
- novacode_cli/agents/provider_caching.py +99 -0
- novacode_cli/agents/server_launcher.py +904 -0
- novacode_cli/agents/strict_tools.py +384 -0
- novacode_cli/agents/task_discipline.py +294 -0
- novacode_cli/agents/tool_call_repair.py +290 -0
- novacode_cli/agents/tool_offload.py +291 -0
- novacode_cli/agents/tool_search.py +806 -0
- novacode_cli/agents/tool_search_variants.py +106 -0
- novacode_cli/agents/tool_verdicts.py +1194 -0
- novacode_cli/agents/user_async_agents.py +312 -0
- novacode_cli/artifacts/__init__.py +10 -0
- novacode_cli/artifacts/registry.py +278 -0
- novacode_cli/artifacts/server.py +426 -0
- novacode_cli/assets/Nova.png +0 -0
- novacode_cli/assets/nova-avatar.png +0 -0
- novacode_cli/audio/__init__.py +301 -0
- novacode_cli/audio/capture.py +137 -0
- novacode_cli/audio/downloads.py +273 -0
- novacode_cli/audio/pipeline.py +391 -0
- novacode_cli/audio/playback.py +87 -0
- novacode_cli/audio/providers.py +208 -0
- novacode_cli/audio/speakable.py +78 -0
- novacode_cli/audio/stt.py +132 -0
- novacode_cli/audio/stt_deepgram.py +69 -0
- novacode_cli/audio/stt_elevenlabs.py +89 -0
- novacode_cli/audio/stt_parakeet.py +94 -0
- novacode_cli/audio/stt_whistle.py +61 -0
- novacode_cli/audio/summarize.py +92 -0
- novacode_cli/audio/tts.py +85 -0
- novacode_cli/audio/tts_kokoro.py +73 -0
- novacode_cli/audio/tts_orpheus.py +180 -0
- novacode_cli/audio/tts_pocket.py +131 -0
- novacode_cli/audio/tts_sano.py +69 -0
- novacode_cli/audio/vad.py +145 -0
- novacode_cli/backends/__init__.py +17 -0
- novacode_cli/backends/filesystem.py +894 -0
- novacode_cli/backends/native_search.py +92 -0
- novacode_cli/bootstrap/__init__.py +9 -0
- novacode_cli/bootstrap/env_snapshot.py +240 -0
- novacode_cli/bootstrap/steering.py +464 -0
- novacode_cli/bootstrap/vision_router.py +604 -0
- novacode_cli/brand.py +362 -0
- novacode_cli/cli_args.py +718 -0
- novacode_cli/commands/__init__.py +237 -0
- novacode_cli/commands/agents_commands.py +231 -0
- novacode_cli/commands/auth_handler.py +40 -0
- novacode_cli/commands/browser_use_handler.py +75 -0
- novacode_cli/commands/cache_handler.py +32 -0
- novacode_cli/commands/chat_handler.py +1357 -0
- novacode_cli/commands/ci_fix.py +171 -0
- novacode_cli/commands/claude_plan.py +331 -0
- novacode_cli/commands/codex_plan.py +195 -0
- novacode_cli/commands/commands.py +1113 -0
- novacode_cli/commands/create_handler.py +124 -0
- novacode_cli/commands/create_server.py +1887 -0
- novacode_cli/commands/cron_handler.py +119 -0
- novacode_cli/commands/dream_handler.py +203 -0
- novacode_cli/commands/effort_handler.py +65 -0
- novacode_cli/commands/evolution_handler.py +214 -0
- novacode_cli/commands/file_commands.py +381 -0
- novacode_cli/commands/handoff_handler.py +68 -0
- novacode_cli/commands/hooks_handler.py +269 -0
- novacode_cli/commands/init_handler.py +146 -0
- novacode_cli/commands/learning_handler.py +160 -0
- novacode_cli/commands/lite_mode_handler.py +76 -0
- novacode_cli/commands/log_commands.py +363 -0
- novacode_cli/commands/mcp_handler.py +29 -0
- novacode_cli/commands/model_handler.py +29 -0
- novacode_cli/commands/notifications_handler.py +158 -0
- novacode_cli/commands/onboard_handler.py +121 -0
- novacode_cli/commands/plan_handler.py +440 -0
- novacode_cli/commands/planner_usage.py +250 -0
- novacode_cli/commands/plugin_install_handler.py +38 -0
- novacode_cli/commands/plugins_handler.py +35 -0
- novacode_cli/commands/prime_handler.py +455 -0
- novacode_cli/commands/prompt_handler.py +149 -0
- novacode_cli/commands/provider_usage.py +160 -0
- novacode_cli/commands/ralph_events.py +106 -0
- novacode_cli/commands/ralph_handler.py +1293 -0
- novacode_cli/commands/refine_handler.py +213 -0
- novacode_cli/commands/research_handler.py +202 -0
- novacode_cli/commands/rootapp_remote.py +83 -0
- novacode_cli/commands/router_handler.py +36 -0
- novacode_cli/commands/server_commands.py +249 -0
- novacode_cli/commands/session_commands.py +203 -0
- novacode_cli/commands/share_handler.py +450 -0
- novacode_cli/commands/side_commands.py +366 -0
- novacode_cli/commands/skill_invoke.py +420 -0
- novacode_cli/commands/skills_commands.py +48 -0
- novacode_cli/commands/trace_handler.py +322 -0
- novacode_cli/commands/trello_handler.py +226 -0
- novacode_cli/commands/trello_remote.py +177 -0
- novacode_cli/commands/trello_server.py +1214 -0
- novacode_cli/commands/vision_handler.py +127 -0
- novacode_cli/commands/voice_handler.py +447 -0
- novacode_cli/commands/webhook_handler.py +134 -0
- novacode_cli/commands/wiki_commands.py +227 -0
- novacode_cli/compaction.py +753 -0
- novacode_cli/computation_cache.py +159 -0
- novacode_cli/computer/__init__.py +1 -0
- novacode_cli/computer/persistent.py +234 -0
- novacode_cli/computer/preview.py +253 -0
- novacode_cli/computer/runtime.py +474 -0
- novacode_cli/computer/windows_console.py +152 -0
- novacode_cli/computer/windows_qemu.py +211 -0
- novacode_cli/config/browser_mode.py +159 -0
- novacode_cli/config/catalog_models.py +95 -0
- novacode_cli/config/config.py +1388 -0
- novacode_cli/config/credentials.py +540 -0
- novacode_cli/config/google_oauth_auth.py +281 -0
- novacode_cli/config/model_capabilities.py +343 -0
- novacode_cli/config/model_catalog.py +275 -0
- novacode_cli/config/model_create.py +1325 -0
- novacode_cli/config/model_manager.py +695 -0
- novacode_cli/config/nova_config.py +1283 -0
- novacode_cli/config/openai_chatgpt_auth.py +384 -0
- novacode_cli/config/opencode_gateway.py +25 -0
- novacode_cli/config/plan_mode.py +188 -0
- novacode_cli/config/provider_auth.py +438 -0
- novacode_cli/config/provider_catalog.json +5127 -0
- novacode_cli/config/provider_registry.py +38 -0
- novacode_cli/config/provider_transports.py +258 -0
- novacode_cli/config/role_models.py +175 -0
- novacode_cli/context/__init__.py +50 -0
- novacode_cli/context/_analysis.py +799 -0
- novacode_cli/context/_budget.py +116 -0
- novacode_cli/context/_conversation.py +73 -0
- novacode_cli/context/_dynamic.py +264 -0
- novacode_cli/context/_model_config.py +380 -0
- novacode_cli/context/_models_dev.py +99 -0
- novacode_cli/context/archives.py +248 -0
- novacode_cli/context/estimates.py +65 -0
- novacode_cli/context/history.py +17 -0
- novacode_cli/context/knowledge_cache.py +292 -0
- novacode_cli/context/manager.py +101 -0
- novacode_cli/context/observations.py +65 -0
- novacode_cli/context/pressure.py +168 -0
- novacode_cli/context/provider_accounting.py +109 -0
- novacode_cli/context/snapshot.py +493 -0
- novacode_cli/context/strategies.py +279 -0
- novacode_cli/context/task_progress.py +122 -0
- novacode_cli/core/__init__.py +0 -0
- novacode_cli/core/agent_loop.py +1821 -0
- novacode_cli/core/autonomous_loop.py +277 -0
- novacode_cli/core/code_approvals.py +57 -0
- novacode_cli/core/input_preparation.py +276 -0
- novacode_cli/core/reasoning.py +67 -0
- novacode_cli/core/streaming.py +326 -0
- novacode_cli/core/subagent_tasks.py +696 -0
- novacode_cli/core/subagent_tracking.py +235 -0
- novacode_cli/core/tool_results.py +116 -0
- novacode_cli/core/verification_loop.py +272 -0
- novacode_cli/council.py +595 -0
- novacode_cli/council_consensus.py +115 -0
- novacode_cli/council_decision.py +157 -0
- novacode_cli/council_planner.py +1421 -0
- novacode_cli/council_settings.py +208 -0
- novacode_cli/council_web_settings.py +41 -0
- novacode_cli/council_workspace.py +56 -0
- novacode_cli/cowork/__init__.py +10 -0
- novacode_cli/cowork/broker_middleware.py +162 -0
- novacode_cli/cowork/launcher.py +102 -0
- novacode_cli/cowork/policy.py +284 -0
- novacode_cli/cowork/ui.py +826 -0
- novacode_cli/daemons/__init__.py +1 -0
- novacode_cli/daemons/registry.py +342 -0
- novacode_cli/desktop_notifications.py +302 -0
- novacode_cli/doctor.py +251 -0
- novacode_cli/embeddings.py +109 -0
- novacode_cli/entrypoint.py +56 -0
- novacode_cli/errors/__init__.py +19 -0
- novacode_cli/errors/handlers.py +648 -0
- novacode_cli/errors/provider_errors.py +249 -0
- novacode_cli/errors/taxonomy.py +80 -0
- novacode_cli/events.py +179 -0
- novacode_cli/file_ops.py +714 -0
- novacode_cli/git_safety.py +281 -0
- novacode_cli/handoff.py +383 -0
- novacode_cli/headless/__init__.py +18 -0
- novacode_cli/headless/input.py +58 -0
- novacode_cli/headless/output.py +224 -0
- novacode_cli/headless/pipe.py +532 -0
- novacode_cli/headless/runner.py +461 -0
- novacode_cli/hermes/__init__.py +39 -0
- novacode_cli/hermes/activation.py +297 -0
- novacode_cli/hermes/config.py +129 -0
- novacode_cli/hermes/curator.py +199 -0
- novacode_cli/hermes/evolution.py +374 -0
- novacode_cli/hermes/ledger.py +364 -0
- novacode_cli/hermes/memory_tiers.py +969 -0
- novacode_cli/hermes/middleware.py +678 -0
- novacode_cli/hermes/overview.py +174 -0
- novacode_cli/hermes/prompt_evolution.py +503 -0
- novacode_cli/hermes/refine_loop.py +643 -0
- novacode_cli/hermes/refinement_log.py +190 -0
- novacode_cli/hermes/review.py +674 -0
- novacode_cli/hermes/skill_debate.py +135 -0
- novacode_cli/hermes/skill_discovery.py +450 -0
- novacode_cli/hermes/skill_manager.py +335 -0
- novacode_cli/hermes/skill_proposals.py +43 -0
- novacode_cli/hermes/test_gaming.py +119 -0
- novacode_cli/hermes/tracker.py +435 -0
- novacode_cli/hermes/tuner.py +206 -0
- novacode_cli/hermes/verification_memory.py +134 -0
- novacode_cli/hermes/verifier.py +277 -0
- novacode_cli/hitl/__init__.py +22 -0
- novacode_cli/hitl/interrupts.py +493 -0
- novacode_cli/hooks.py +359 -0
- novacode_cli/image_utils.py +517 -0
- novacode_cli/init/__init__.py +7 -0
- novacode_cli/init/events.py +43 -0
- novacode_cli/input_utils.py +526 -0
- novacode_cli/install_commands.py +87 -0
- novacode_cli/integrations/__init__.py +82 -0
- novacode_cli/integrations/daytona.py +122 -0
- novacode_cli/integrations/docker.py +309 -0
- novacode_cli/integrations/host_path.py +111 -0
- novacode_cli/integrations/langsmith.py +151 -0
- novacode_cli/integrations/modal.py +134 -0
- novacode_cli/integrations/runloop.py +130 -0
- novacode_cli/integrations/sandbox_factory.py +1388 -0
- novacode_cli/integrations/sandbox_registry.py +312 -0
- novacode_cli/integrations/workdir_backend.py +589 -0
- novacode_cli/main.py +2078 -0
- novacode_cli/mcp/__init__.py +73 -0
- novacode_cli/mcp/auth.py +189 -0
- novacode_cli/mcp/client.py +571 -0
- novacode_cli/mcp/code_mode.py +5 -0
- novacode_cli/mcp/code_runtime.py +5 -0
- novacode_cli/mcp/commands.py +902 -0
- novacode_cli/mcp/config.py +406 -0
- novacode_cli/mcp/generic.py +128 -0
- novacode_cli/mcp/middleware.py +993 -0
- novacode_cli/mcp/presets.py +367 -0
- novacode_cli/memory/__init__.py +1 -0
- novacode_cli/memory/agent_memory.py +1312 -0
- novacode_cli/memory/builtin_lessons.py +60 -0
- novacode_cli/memory/dream_gates.py +192 -0
- novacode_cli/memory/limits.py +59 -0
- novacode_cli/memory/parsed_cache.py +106 -0
- novacode_cli/memory/recall_stats.py +97 -0
- novacode_cli/memory/semantic.py +116 -0
- novacode_cli/memory/store.py +335 -0
- novacode_cli/memory/wiki.py +274 -0
- novacode_cli/migrate.py +185 -0
- novacode_cli/onboarding.py +649 -0
- novacode_cli/onboarding_check.py +119 -0
- novacode_cli/path_approval.py +297 -0
- novacode_cli/plan_archive.py +148 -0
- novacode_cli/plugins/__init__.py +121 -0
- novacode_cli/plugins/claude_plugins.py +538 -0
- novacode_cli/plugins/cli.py +168 -0
- novacode_cli/plugins/components.py +413 -0
- novacode_cli/plugins/hooks.py +356 -0
- novacode_cli/plugins/loader.py +317 -0
- novacode_cli/plugins/marketplaces.py +199 -0
- novacode_cli/plugins/storage.py +70 -0
- novacode_cli/prime.py +759 -0
- novacode_cli/prime_factory/__init__.py +15 -0
- novacode_cli/prime_factory/container.py +198 -0
- novacode_cli/prime_factory/controller.py +565 -0
- novacode_cli/prime_factory/knowledge.py +261 -0
- novacode_cli/prime_factory/publication.py +218 -0
- novacode_cli/prime_factory/recovery.py +50 -0
- novacode_cli/prime_factory/research.py +370 -0
- novacode_cli/prime_factory/runner.py +608 -0
- novacode_cli/prime_factory/runtime.py +261 -0
- novacode_cli/prime_factory/state.py +201 -0
- novacode_cli/prime_factory/store.py +128 -0
- novacode_cli/prime_factory/worker.py +129 -0
- novacode_cli/prime_factory/workflow.py +899 -0
- novacode_cli/prime_factory/workspace.py +317 -0
- novacode_cli/prime_memory.py +290 -0
- novacode_cli/process_manager.py +585 -0
- novacode_cli/project_onboard.py +537 -0
- novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
- novacode_cli/prompts/__init__.py +168 -0
- novacode_cli/prompts/agent_definitions.jinja +11 -0
- novacode_cli/prompts/agent_generation.jinja +125 -0
- novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
- novacode_cli/prompts/claude_plan_mode.jinja +12 -0
- novacode_cli/prompts/coding_workflow.jinja +18 -0
- novacode_cli/prompts/computer_use.jinja +17 -0
- novacode_cli/prompts/core_agent_system.jinja +81 -0
- novacode_cli/prompts/delegation.jinja +15 -0
- novacode_cli/prompts/desktop_power.jinja +4 -0
- novacode_cli/prompts/file_paths.jinja +15 -0
- novacode_cli/prompts/file_tracker.jinja +23 -0
- novacode_cli/prompts/init_agents_md.jinja +56 -0
- novacode_cli/prompts/init_exploration.jinja +174 -0
- novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
- novacode_cli/prompts/instruction_scope.jinja +5 -0
- novacode_cli/prompts/learnings_extraction.jinja +45 -0
- novacode_cli/prompts/longterm_memory.jinja +54 -0
- novacode_cli/prompts/mcp.jinja +84 -0
- novacode_cli/prompts/memory_consolidation.jinja +40 -0
- novacode_cli/prompts/nova_evolution.jinja +73 -0
- novacode_cli/prompts/nova_review.jinja +158 -0
- novacode_cli/prompts/nova_skill_debate.jinja +36 -0
- novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
- novacode_cli/prompts/nova_verify.jinja +100 -0
- novacode_cli/prompts/onboard_dashboard.jinja +378 -0
- novacode_cli/prompts/plan_agent.jinja +87 -0
- novacode_cli/prompts/plan_scouts.jinja +5 -0
- novacode_cli/prompts/plan_storage.jinja +8 -0
- novacode_cli/prompts/planning.jinja +82 -0
- novacode_cli/prompts/ponytail/LICENSE +21 -0
- novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
- novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
- novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
- novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
- novacode_cli/prompts/prompt_evolution.jinja +27 -0
- novacode_cli/prompts/ralph_iteration.jinja +75 -0
- novacode_cli/prompts/refine_gate.jinja +31 -0
- novacode_cli/prompts/refine_plan.jinja +64 -0
- novacode_cli/prompts/refine_review.jinja +34 -0
- novacode_cli/prompts/research_delivery.jinja +9 -0
- novacode_cli/prompts/research_swarm.jinja +130 -0
- novacode_cli/prompts/response_style.jinja +5 -0
- novacode_cli/prompts/session_summarization.jinja +86 -0
- novacode_cli/prompts/skill_creation.jinja +85 -0
- novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
- novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
- novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
- novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
- novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
- novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
- novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
- novacode_cli/prompts/summarization.jinja +107 -0
- novacode_cli/prompts/summarization_answers.jinja +23 -0
- novacode_cli/prompts/summarization_questions.jinja +32 -0
- novacode_cli/prompts/vision_caption.jinja +21 -0
- novacode_cli/prompts/voice_summary.jinja +65 -0
- novacode_cli/prompts/wiki_file.jinja +57 -0
- novacode_cli/prompts/wiki_ingest.jinja +87 -0
- novacode_cli/recovery.py +383 -0
- novacode_cli/remote/__init__.py +45 -0
- novacode_cli/remote/approvals.py +282 -0
- novacode_cli/remote/async_task_clients.py +97 -0
- novacode_cli/remote/async_task_watcher.py +240 -0
- novacode_cli/remote/bridge.py +905 -0
- novacode_cli/remote/command_menu.py +26 -0
- novacode_cli/remote/command_ui.py +243 -0
- novacode_cli/remote/config.py +199 -0
- novacode_cli/remote/discord_bridge.py +530 -0
- novacode_cli/remote/discord_format.py +31 -0
- novacode_cli/remote/discord_threads.py +225 -0
- novacode_cli/remote/images.py +97 -0
- novacode_cli/remote/processor.py +601 -0
- novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
- novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
- novacode_cli/remote/rootapp_bot/package.json +18 -0
- novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
- novacode_cli/remote/rootapp_bridge.py +370 -0
- novacode_cli/remote/rootapp_setup.py +89 -0
- novacode_cli/remote/routing.py +103 -0
- novacode_cli/remote/scheduler.py +302 -0
- novacode_cli/remote/session.py +164 -0
- novacode_cli/remote/status.py +411 -0
- novacode_cli/remote/streaming.py +188 -0
- novacode_cli/remote/telegram_bridge.py +1118 -0
- novacode_cli/remote/telegram_format.py +215 -0
- novacode_cli/remote/telegram_hub.py +256 -0
- novacode_cli/remote/voice_notes.py +120 -0
- novacode_cli/remote/webhook_adapters.py +167 -0
- novacode_cli/remote/webhook_server.py +154 -0
- novacode_cli/rewind.py +150 -0
- novacode_cli/rooms/__init__.py +1 -0
- novacode_cli/rooms/bridge.py +86 -0
- novacode_cli/rooms/cli.py +231 -0
- novacode_cli/rooms/nova.py +227 -0
- novacode_cli/rooms/runtime.py +299 -0
- novacode_cli/rooms/store.py +538 -0
- novacode_cli/rooms/workspace.py +94 -0
- novacode_cli/security/__init__.py +29 -0
- novacode_cli/security/approval_context.py +45 -0
- novacode_cli/security/delegated_approval.py +68 -0
- novacode_cli/security/local_http.py +85 -0
- novacode_cli/security/middleware.py +87 -0
- novacode_cli/security/paths.py +35 -0
- novacode_cli/security/policy.py +535 -0
- novacode_cli/security/policy_writer.py +90 -0
- novacode_cli/security/prompt_injection.py +203 -0
- novacode_cli/security/remember.py +81 -0
- novacode_cli/security/rule_synthesis.py +116 -0
- novacode_cli/security/secret_files.py +61 -0
- novacode_cli/security/session_allow.py +114 -0
- novacode_cli/security/unicode_security.py +516 -0
- novacode_cli/security/validator.py +173 -0
- novacode_cli/server/__init__.py +9 -0
- novacode_cli/server/app.py +632 -0
- novacode_cli/server/event_adapter.py +189 -0
- novacode_cli/server/session_manager.py +90 -0
- novacode_cli/server_main.py +64 -0
- novacode_cli/server_runner/dev_server.py +530 -0
- novacode_cli/server_runner/test_runner.py +470 -0
- novacode_cli/session/__init__.py +1 -0
- novacode_cli/session/adapters/__init__.py +240 -0
- novacode_cli/session/adapters/claude_code.py +102 -0
- novacode_cli/session/adapters/codex.py +77 -0
- novacode_cli/session/adapters/harness_native.py +75 -0
- novacode_cli/session/checkpoint_coordinator.py +448 -0
- novacode_cli/session/context_restore.py +25 -0
- novacode_cli/session/history_export.py +228 -0
- novacode_cli/session/history_tree.py +174 -0
- novacode_cli/session/imported_context.py +267 -0
- novacode_cli/session/runtime_identity.py +32 -0
- novacode_cli/session/session_persistence.py +1168 -0
- novacode_cli/session/session_prompt_builder.py +279 -0
- novacode_cli/session/session_restore.py +351 -0
- novacode_cli/session/session_summarization.py +20 -0
- novacode_cli/session/transcript_journal.py +153 -0
- novacode_cli/sessions/__init__.py +11 -0
- novacode_cli/sessions/gateway.py +84 -0
- novacode_cli/sessions/launch.py +51 -0
- novacode_cli/sessions/lease.py +213 -0
- novacode_cli/sessions/messaging.py +255 -0
- novacode_cli/sessions/protocol.py +216 -0
- novacode_cli/sessions/resume.py +31 -0
- novacode_cli/sessions/supervisor.py +498 -0
- novacode_cli/sessions/tasks.py +73 -0
- novacode_cli/sessions/tui_messaging.py +413 -0
- novacode_cli/sessions/worker.py +652 -0
- novacode_cli/sessions/worktree.py +262 -0
- novacode_cli/shell/__init__.py +27 -0
- novacode_cli/shell/jobs.py +475 -0
- novacode_cli/shell/middleware.py +1568 -0
- novacode_cli/shell/os_sandbox.py +205 -0
- novacode_cli/shell/output.py +181 -0
- novacode_cli/shell/patterns.py +275 -0
- novacode_cli/shell/processes.py +87 -0
- novacode_cli/shell/utils.py +236 -0
- novacode_cli/skills/__init__.py +26 -0
- novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
- novacode_cli/skills/curation_middleware.py +69 -0
- novacode_cli/skills/executable.py +161 -0
- novacode_cli/skills/libraries.py +102 -0
- novacode_cli/skills/load.py +248 -0
- novacode_cli/skills/prune.py +215 -0
- novacode_cli/skills/refreshing_middleware.py +896 -0
- novacode_cli/skills/retrieval.py +248 -0
- novacode_cli/skills/runtime.py +118 -0
- novacode_cli/skills/schema.py +254 -0
- novacode_cli/skills/skill_creation.py +1549 -0
- novacode_cli/skills/skill_lock.py +75 -0
- novacode_cli/skills/skill_system_prompt.py +16 -0
- novacode_cli/skills/skills_prefs.py +126 -0
- novacode_cli/skills/tool_resolver.py +31 -0
- novacode_cli/skills/upstream.py +50 -0
- novacode_cli/skills/versioning.py +291 -0
- novacode_cli/states/Session.py +847 -0
- novacode_cli/states/__init__.py +31 -0
- novacode_cli/states/slices/__init__.py +17 -0
- novacode_cli/states/slices/agent_runtime.py +249 -0
- novacode_cli/states/slices/background_tasks.py +21 -0
- novacode_cli/states/slices/notifications.py +74 -0
- novacode_cli/states/slices/remote_bridge.py +56 -0
- novacode_cli/states/slices/ui_settings.py +49 -0
- novacode_cli/states/slices/wiki.py +25 -0
- novacode_cli/teammates/__init__.py +1 -0
- novacode_cli/teammates/controller.py +557 -0
- novacode_cli/teammates/graph.py +104 -0
- novacode_cli/teammates/registry.py +175 -0
- novacode_cli/teammates/service.py +254 -0
- novacode_cli/teammates/worker.py +19 -0
- novacode_cli/teams/__init__.py +1 -0
- novacode_cli/teams/adapters/__init__.py +1 -0
- novacode_cli/teams/adapters/container.py +316 -0
- novacode_cli/teams/adapters/inprocess.py +206 -0
- novacode_cli/teams/adapters/process.py +364 -0
- novacode_cli/teams/bootstrap.py +246 -0
- novacode_cli/teams/container_git.py +67 -0
- novacode_cli/teams/context.py +34 -0
- novacode_cli/teams/coordinator.py +815 -0
- novacode_cli/teams/gateway.py +200 -0
- novacode_cli/teams/host.py +64 -0
- novacode_cli/teams/legacy.py +52 -0
- novacode_cli/teams/middleware.py +176 -0
- novacode_cli/teams/models.py +104 -0
- novacode_cli/teams/output.py +47 -0
- novacode_cli/teams/protocol.py +50 -0
- novacode_cli/teams/recovery.py +80 -0
- novacode_cli/teams/scheduler.py +44 -0
- novacode_cli/teams/store.py +631 -0
- novacode_cli/teams/worker.py +251 -0
- novacode_cli/teams/workspace.py +100 -0
- novacode_cli/token_utils.py +256 -0
- novacode_cli/tools/__init__.py +104 -0
- novacode_cli/tools/_shared.py +97 -0
- novacode_cli/tools/_types.py +24 -0
- novacode_cli/tools/artifact_tools.py +137 -0
- novacode_cli/tools/code_search_tools.py +330 -0
- novacode_cli/tools/computer_tools.py +77 -0
- novacode_cli/tools/cron_tools.py +65 -0
- novacode_cli/tools/daemon_tool.py +290 -0
- novacode_cli/tools/desktop_tools.py +161 -0
- novacode_cli/tools/fetch_tools.py +530 -0
- novacode_cli/tools/http_tools.py +279 -0
- novacode_cli/tools/inspection_tools.py +107 -0
- novacode_cli/tools/job_tools.py +143 -0
- novacode_cli/tools/knowledge_tools.py +427 -0
- novacode_cli/tools/memory_tools.py +461 -0
- novacode_cli/tools/memory_wiki_tools.py +40 -0
- novacode_cli/tools/notebook_tools.py +238 -0
- novacode_cli/tools/oracle_tool.py +83 -0
- novacode_cli/tools/plan_archive_tools.py +80 -0
- novacode_cli/tools/plan_mode_tools.py +315 -0
- novacode_cli/tools/python_kernel_loop.py +165 -0
- novacode_cli/tools/python_kernel_tool.py +222 -0
- novacode_cli/tools/reflection_tools.py +34 -0
- novacode_cli/tools/scraper_tools.py +377 -0
- novacode_cli/tools/skill_tools.py +275 -0
- novacode_cli/tools/speak_tool.py +51 -0
- novacode_cli/tools/store_memory_tools.py +227 -0
- novacode_cli/tools/ui_tools.py +62 -0
- novacode_cli/tools/waiter_tools.py +408 -0
- novacode_cli/tools/web_tools.py +349 -0
- novacode_cli/tools/wiki_tools.py +152 -0
- novacode_cli/tools/worktree_tools.py +219 -0
- novacode_cli/tracking/__init__.py +10 -0
- novacode_cli/tracking/file_tracker.py +826 -0
- novacode_cli/tracking/local_metrics.py +170 -0
- novacode_cli/tracking/loop_guard.py +507 -0
- novacode_cli/tracking/request_metrics.py +60 -0
- novacode_cli/tracking/tracing.py +692 -0
- novacode_cli/tracking/usage_tree.py +677 -0
- novacode_cli/tracking/workspace_anchoring.py +231 -0
- novacode_cli/tui/__init__.py +11 -0
- novacode_cli/tui/animation_rate.py +9 -0
- novacode_cli/tui/animations.py +332 -0
- novacode_cli/tui/app.py +19315 -0
- novacode_cli/tui/auth_display.py +87 -0
- novacode_cli/tui/auth_screens.py +826 -0
- novacode_cli/tui/avatar.py +294 -0
- novacode_cli/tui/avatar_frames.py +21 -0
- novacode_cli/tui/background_tasks.py +35 -0
- novacode_cli/tui/browser_settings_screen.py +70 -0
- novacode_cli/tui/caveman_screen.py +185 -0
- novacode_cli/tui/council_settings_screen.py +182 -0
- novacode_cli/tui/council_view.py +535 -0
- novacode_cli/tui/decision_settings.py +268 -0
- novacode_cli/tui/diff_preview.py +199 -0
- novacode_cli/tui/gc_tuning.py +63 -0
- novacode_cli/tui/handoff_screen.py +206 -0
- novacode_cli/tui/harness.py +244 -0
- novacode_cli/tui/history_tree_screen.py +179 -0
- novacode_cli/tui/home.py +328 -0
- novacode_cli/tui/home_scene.py +94 -0
- novacode_cli/tui/icons.py +81 -0
- novacode_cli/tui/interface_styles.tcss +327 -0
- novacode_cli/tui/matrix3.py +108 -0
- novacode_cli/tui/mcp_tools_screen.py +131 -0
- novacode_cli/tui/motion.py +178 -0
- novacode_cli/tui/output_body.py +136 -0
- novacode_cli/tui/output_buffer.py +59 -0
- novacode_cli/tui/palette.py +334 -0
- novacode_cli/tui/pickers.py +237 -0
- novacode_cli/tui/prime_panel.py +506 -0
- novacode_cli/tui/provider_signin.py +213 -0
- novacode_cli/tui/pty_shell.py +159 -0
- novacode_cli/tui/reference_screens.py +444 -0
- novacode_cli/tui/resume_picker.py +346 -0
- novacode_cli/tui/rooms.py +597 -0
- novacode_cli/tui/screens.py +7088 -0
- novacode_cli/tui/session_import.py +333 -0
- novacode_cli/tui/session_pane.py +242 -0
- novacode_cli/tui/session_tab.py +42 -0
- novacode_cli/tui/session_transcript.py +118 -0
- novacode_cli/tui/shell_output.py +7 -0
- novacode_cli/tui/stall_watch.py +174 -0
- novacode_cli/tui/statusline.py +40 -0
- novacode_cli/tui/statusline_screen.py +91 -0
- novacode_cli/tui/teammate_updates.py +162 -0
- novacode_cli/tui/teammates.py +548 -0
- novacode_cli/tui/terminal.py +289 -0
- novacode_cli/tui/terminal_view.py +235 -0
- novacode_cli/tui/themes.py +118 -0
- novacode_cli/tui/update_notice.py +84 -0
- novacode_cli/tui/voice_downloads.py +113 -0
- novacode_cli/tui/widgets.py +2038 -0
- novacode_cli/tui/windows_input.py +38 -0
- novacode_cli/tui/workspace_approval.py +165 -0
- novacode_cli/ui/council_web.py +113 -0
- novacode_cli/ui/council_workspace.py +72 -0
- novacode_cli/ui/cowork_web.py +41 -0
- novacode_cli/ui/diff_highlight.py +160 -0
- novacode_cli/ui/execution.py +1396 -0
- novacode_cli/ui/exit_summary.py +65 -0
- novacode_cli/ui/hitl_approval.py +196 -0
- novacode_cli/ui/input_preparation.py +2 -0
- novacode_cli/ui/interrupt_handlers.py +215 -0
- novacode_cli/ui/question_prompt.py +151 -0
- novacode_cli/ui/status_phrases.py +157 -0
- novacode_cli/ui/stream_preview.py +66 -0
- novacode_cli/ui/streaming.py +2 -0
- novacode_cli/ui/subagent_tracking.py +2 -0
- novacode_cli/ui/ui_elements.py +1585 -0
- novacode_cli/ui/web_workspace.py +244 -0
- novacode_cli/ui_events.py +299 -0
- novacode_cli/update_ui.py +243 -0
- novacode_cli/updates.py +489 -0
- novacode_cli/utils/backend_patches.py +399 -0
- novacode_cli/utils/backend_paths.py +467 -0
- novacode_cli/utils/lazy_providers.py +51 -0
- novacode_cli/utils/model_info.py +223 -0
- novacode_cli/utils/pdf_extraction.py +183 -0
- novacode_cli/utils/startup.py +29 -0
- novacode_cli/utils/tasks.py +42 -0
- novacode_cli/utils/windows.py +9 -0
- novacode_cli/vixie/hook_adapter.py +127 -0
- novacode_cli/vixie/server.py +375 -0
- novacode_cli/wiki/__init__.py +15 -0
- novacode_cli/wiki/ask.py +56 -0
- novacode_cli/wiki/file.py +71 -0
- novacode_cli/wiki/ingest.py +184 -0
- novacode_cli/wiki/manager.py +261 -0
- novacode_cli-1.2.0.dist-info/METADATA +271 -0
- novacode_cli-1.2.0.dist-info/RECORD +691 -0
- novacode_cli-1.2.0.dist-info/WHEEL +5 -0
- novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
- novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
- novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
"""Row data for the dynamic-subagents panel: pure mapping and formatting.
|
|
2
|
+
|
|
3
|
+
Three sources feed the panel and they disagree about shape, so the translation
|
|
4
|
+
lives here rather than in the renderer:
|
|
5
|
+
|
|
6
|
+
* ``langchain-quickjs`` publishes a ``subagent`` event per ``task()`` dispatch on
|
|
7
|
+
LangGraph's ``custom`` stream (start / complete / error, with ``id``,
|
|
8
|
+
``eval_id``, ``subagent_type``, ``label``, ``description``, ``duration_ms`` and
|
|
9
|
+
``error``). One fan-out shares an ``eval_id``, which is what the panel shows as
|
|
10
|
+
a phase.
|
|
11
|
+
* The sync ``task`` tool is tracked by :class:`SubagentTracker`, which records a
|
|
12
|
+
start time per dispatch.
|
|
13
|
+
* Async (remote) tasks come from graph state as plain dicts with a status and an
|
|
14
|
+
agent name, and no duration at all.
|
|
15
|
+
|
|
16
|
+
Everything here is pure: no Textual, no live app, so the column layout and the
|
|
17
|
+
event mapping can be tested without a terminal.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
import time
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import TYPE_CHECKING, Any
|
|
26
|
+
|
|
27
|
+
from rich.text import Text
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from novacode_cli.ui_events import SubagentTask
|
|
31
|
+
|
|
32
|
+
#: The discriminator the quickjs bridge stamps on its fan-out events
|
|
33
|
+
#: (``langchain_quickjs._subagent.SUBAGENT_STREAM_EVENT_TYPE``). Compared as a
|
|
34
|
+
#: literal so this module never needs the package imported to be testable.
|
|
35
|
+
CUSTOM_EVENT_TYPE = "subagent"
|
|
36
|
+
|
|
37
|
+
#: Run-config metadata key naming the panel row a subagent's stream belongs to.
|
|
38
|
+
TASK_ID_METADATA_KEY = "nova_task_id"
|
|
39
|
+
|
|
40
|
+
#: The phase id async (remote) tasks are grouped under.
|
|
41
|
+
ASYNC_PHASE = "async"
|
|
42
|
+
|
|
43
|
+
#: How a row was launched. Drives the phase grouping's fallback label.
|
|
44
|
+
PHASE_KINDS = ("eval", "direct", "async", "team")
|
|
45
|
+
|
|
46
|
+
#: Status glyphs, matching the vocabulary the rest of the TUI uses (✓ / ✗ from
|
|
47
|
+
#: the tool cards, the hourglass from a running tool).
|
|
48
|
+
STATUS_GLYPH = {"running": "⏳", "done": "✓", "failed": "✗", "stopped": "■"}
|
|
49
|
+
|
|
50
|
+
#: Width of the phases pane, and the caps that keep a 32-way fan-out readable.
|
|
51
|
+
PHASE_COLUMN = 18
|
|
52
|
+
MODEL_COLUMN = 18
|
|
53
|
+
TIME_COLUMN = 8
|
|
54
|
+
MAX_ROWS = 200
|
|
55
|
+
MAX_PHASES = 20
|
|
56
|
+
MAX_PREVIEW_ENTRIES = 400
|
|
57
|
+
|
|
58
|
+
#: Layout thresholds, named so the intent survives a later tweak.
|
|
59
|
+
WIDE_PANEL = 70 # a pane this wide can afford a real model column
|
|
60
|
+
TWO_PANE_WIDTH = 60 # below this the phases pane is dropped instead of squeezed
|
|
61
|
+
SECONDS_PER_MINUTE = 60
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def quickjs_event_type() -> str:
|
|
65
|
+
"""The bridge's own discriminator, if the package is importable.
|
|
66
|
+
|
|
67
|
+
Falls back to the literal so the panel keeps working when
|
|
68
|
+
``langchain-quickjs`` is missing or has moved its constant.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
from langchain_quickjs._subagent import ( # type: ignore[import-not-found]
|
|
72
|
+
SUBAGENT_STREAM_EVENT_TYPE,
|
|
73
|
+
)
|
|
74
|
+
except Exception: # noqa: BLE001 — feature detection, never a failure
|
|
75
|
+
return CUSTOM_EVENT_TYPE
|
|
76
|
+
return str(SUBAGENT_STREAM_EVENT_TYPE)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def format_elapsed(seconds: float) -> str:
|
|
80
|
+
"""``38.3s`` / ``1m 04s``, the TIME column."""
|
|
81
|
+
if seconds < 0 or math.isnan(seconds): # negative or NaN
|
|
82
|
+
return "—"
|
|
83
|
+
if seconds < SECONDS_PER_MINUTE:
|
|
84
|
+
return f"{seconds:.1f}s"
|
|
85
|
+
minutes, rest = divmod(int(seconds), SECONDS_PER_MINUTE)
|
|
86
|
+
return f"{minutes}m {rest:02d}s"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def elapsed_for(task: SubagentTask, now: float | None = None) -> float:
|
|
90
|
+
"""Seconds a task took, or has been running for.
|
|
91
|
+
|
|
92
|
+
A finished row uses the duration its completing event carried, so the column
|
|
93
|
+
stops moving and matches the bridge's own measurement. A running row ages
|
|
94
|
+
against *now*.
|
|
95
|
+
"""
|
|
96
|
+
if task.duration_ms is not None:
|
|
97
|
+
return task.duration_ms / 1000.0
|
|
98
|
+
if not task.started_at:
|
|
99
|
+
return 0.0
|
|
100
|
+
return max(0.0, (now if now is not None else time.time()) - task.started_at)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def status_glyph(status: str) -> str:
|
|
104
|
+
"""The single-cell mark in front of a row."""
|
|
105
|
+
return STATUS_GLYPH.get(status, "•")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def task_from_custom_event(
|
|
109
|
+
payload: object, *, now: float, model: str | None = None
|
|
110
|
+
) -> SubagentTask | None:
|
|
111
|
+
"""Translate one quickjs ``subagent`` stream event, or ``None`` for anything else.
|
|
112
|
+
|
|
113
|
+
Returning ``None`` for unknown payloads matters: the custom stream carries
|
|
114
|
+
whatever else middleware writes to it, and the loop forwards all of it.
|
|
115
|
+
"""
|
|
116
|
+
from novacode_cli.ui_events import SubagentTask
|
|
117
|
+
|
|
118
|
+
if not isinstance(payload, dict) or payload.get("type") != quickjs_event_type():
|
|
119
|
+
return None
|
|
120
|
+
task_id = str(payload.get("id") or "")
|
|
121
|
+
status = {"start": "running", "complete": "done", "error": "failed"}.get(
|
|
122
|
+
str(payload.get("phase") or "")
|
|
123
|
+
)
|
|
124
|
+
if not task_id or status is None:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
raw_duration = payload.get("duration_ms")
|
|
128
|
+
duration_ms = int(raw_duration) if isinstance(raw_duration, (int, float)) else None
|
|
129
|
+
if status == "running":
|
|
130
|
+
started_at = now
|
|
131
|
+
elif duration_ms is not None:
|
|
132
|
+
# A completion for a dispatch we never saw start (a fan-out already in
|
|
133
|
+
# flight when the panel appeared): back-date it so TIME is still right.
|
|
134
|
+
started_at = now - duration_ms / 1000.0
|
|
135
|
+
else:
|
|
136
|
+
started_at = 0.0
|
|
137
|
+
|
|
138
|
+
eval_id = payload.get("eval_id")
|
|
139
|
+
return SubagentTask(
|
|
140
|
+
task_id=task_id,
|
|
141
|
+
status=status,
|
|
142
|
+
phase_id=str(eval_id) if eval_id else None,
|
|
143
|
+
phase_kind="eval",
|
|
144
|
+
subagent_type=str(payload.get("subagent_type") or ""),
|
|
145
|
+
label=str(payload.get("label") or ""),
|
|
146
|
+
description=str(payload.get("description") or ""),
|
|
147
|
+
started_at=started_at,
|
|
148
|
+
duration_ms=duration_ms,
|
|
149
|
+
error=str(payload.get("error") or ""),
|
|
150
|
+
model=model,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def task_from_sync_dispatch(
|
|
155
|
+
call_id: str,
|
|
156
|
+
subagent_type: str,
|
|
157
|
+
description: str,
|
|
158
|
+
started_at: float,
|
|
159
|
+
*,
|
|
160
|
+
model: str | None = None,
|
|
161
|
+
) -> SubagentTask:
|
|
162
|
+
"""A dispatch through the blocking ``task`` tool."""
|
|
163
|
+
from novacode_cli.ui_events import SubagentTask
|
|
164
|
+
|
|
165
|
+
return SubagentTask(
|
|
166
|
+
task_id=call_id,
|
|
167
|
+
status="running",
|
|
168
|
+
phase_kind="direct",
|
|
169
|
+
subagent_type=subagent_type,
|
|
170
|
+
label=description,
|
|
171
|
+
description=description,
|
|
172
|
+
started_at=started_at,
|
|
173
|
+
model=model,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def task_from_sync_completion(
|
|
178
|
+
call_id: str,
|
|
179
|
+
*,
|
|
180
|
+
now: float,
|
|
181
|
+
ok: bool,
|
|
182
|
+
duration_ms: int | None = None,
|
|
183
|
+
error: str = "",
|
|
184
|
+
started_at: float = 0.0,
|
|
185
|
+
subagent_type: str = "",
|
|
186
|
+
description: str = "",
|
|
187
|
+
model: str | None = None,
|
|
188
|
+
) -> SubagentTask:
|
|
189
|
+
"""The completing half of a blocking ``task`` call."""
|
|
190
|
+
from novacode_cli.ui_events import SubagentTask
|
|
191
|
+
|
|
192
|
+
resolved = duration_ms
|
|
193
|
+
if resolved is None and started_at:
|
|
194
|
+
resolved = int(max(0.0, now - started_at) * 1000)
|
|
195
|
+
return SubagentTask(
|
|
196
|
+
task_id=call_id,
|
|
197
|
+
status="done" if ok else "failed",
|
|
198
|
+
phase_kind="direct",
|
|
199
|
+
subagent_type=subagent_type,
|
|
200
|
+
label=description,
|
|
201
|
+
description=description,
|
|
202
|
+
started_at=started_at,
|
|
203
|
+
duration_ms=resolved,
|
|
204
|
+
error=error,
|
|
205
|
+
model=model,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
#: Async task statuses, as the Agent Protocol reports them.
|
|
210
|
+
_ASYNC_STATUS = {
|
|
211
|
+
"running": "running",
|
|
212
|
+
"pending": "running",
|
|
213
|
+
"queued": "running",
|
|
214
|
+
"success": "done",
|
|
215
|
+
"completed": "done",
|
|
216
|
+
"done": "done",
|
|
217
|
+
"failed": "failed",
|
|
218
|
+
"error": "failed",
|
|
219
|
+
"cancelled": "failed",
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def task_from_async_entry(
|
|
224
|
+
entry: object, *, now: float, model: str | None = None
|
|
225
|
+
) -> SubagentTask | None:
|
|
226
|
+
"""A remote task from the graph's ``async_tasks`` state."""
|
|
227
|
+
from novacode_cli.ui_events import SubagentTask
|
|
228
|
+
|
|
229
|
+
if not isinstance(entry, dict):
|
|
230
|
+
return None
|
|
231
|
+
task_id = str(entry.get("task_id") or entry.get("thread_id") or "")
|
|
232
|
+
if not task_id:
|
|
233
|
+
return None
|
|
234
|
+
status = _ASYNC_STATUS.get(str(entry.get("status") or "").lower(), "running")
|
|
235
|
+
started_at = _parse_timestamp(entry.get("created_at")) or now
|
|
236
|
+
return SubagentTask(
|
|
237
|
+
task_id=f"async:{task_id}",
|
|
238
|
+
status=status,
|
|
239
|
+
phase_id="async",
|
|
240
|
+
phase_kind="async",
|
|
241
|
+
subagent_type=str(entry.get("agent_name") or "async"),
|
|
242
|
+
label=str(entry.get("agent_name") or task_id),
|
|
243
|
+
description=str(entry.get("thread_id") or ""),
|
|
244
|
+
started_at=started_at,
|
|
245
|
+
duration_ms=int(max(0.0, now - started_at) * 1000) if status != "running" else None,
|
|
246
|
+
model=model,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _parse_timestamp(value: object) -> float | None:
|
|
251
|
+
"""Best-effort epoch seconds from an ISO string or a number."""
|
|
252
|
+
if isinstance(value, (int, float)):
|
|
253
|
+
return float(value)
|
|
254
|
+
if not isinstance(value, str) or not value:
|
|
255
|
+
return None
|
|
256
|
+
try:
|
|
257
|
+
from datetime import datetime
|
|
258
|
+
|
|
259
|
+
return datetime.fromisoformat(value).timestamp()
|
|
260
|
+
except Exception: # noqa: BLE001 — a bad timestamp must not lose the row
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def phase_summary(tasks: list[SubagentTask]) -> tuple[int, int, int, int]:
|
|
265
|
+
"""``(done, total, phases, failed)`` for the panel's header."""
|
|
266
|
+
done = sum(1 for task in tasks if task.status == "done")
|
|
267
|
+
failed = sum(1 for task in tasks if task.status == "failed")
|
|
268
|
+
phases = len({task.phase_id for task in tasks if task.phase_id}) or (1 if tasks else 0)
|
|
269
|
+
return done, len(tasks), phases, failed
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def panel_title(tasks: list[SubagentTask], *, collapsed: bool = False) -> Text:
|
|
273
|
+
"""The header line: caret, name, then the counts."""
|
|
274
|
+
done, total, phases, failed = phase_summary(tasks)
|
|
275
|
+
title = Text()
|
|
276
|
+
title.append("▶ " if collapsed else "▼ ")
|
|
277
|
+
title.append("Active subagents", style="bold")
|
|
278
|
+
title.append(" ")
|
|
279
|
+
title.append(
|
|
280
|
+
f"{total} running"
|
|
281
|
+
if tasks and all(task.status == "running" for task in tasks)
|
|
282
|
+
else f"{done}/{total} done",
|
|
283
|
+
style="dim",
|
|
284
|
+
)
|
|
285
|
+
title.append(" · ", style="dim")
|
|
286
|
+
title.append(f"{phases} phase" + ("s" if phases != 1 else ""), style="dim")
|
|
287
|
+
if failed:
|
|
288
|
+
title.append(" · ", style="dim")
|
|
289
|
+
title.append(f"{failed} failed", style="bold red")
|
|
290
|
+
stopped = sum(task.status == "stopped" for task in tasks)
|
|
291
|
+
if stopped:
|
|
292
|
+
title.append(" · ", style="dim")
|
|
293
|
+
title.append(f"{stopped} stopped", style="yellow")
|
|
294
|
+
return title
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def column_widths(width: int) -> tuple[int, int, int]:
|
|
298
|
+
"""``(name, model, time)`` widths for a task row *width* cells wide.
|
|
299
|
+
|
|
300
|
+
The row spends six cells on layout — the glyph, a space, and two two-space
|
|
301
|
+
separators — so the name column absorbs the remainder. Getting this wrong is
|
|
302
|
+
an invisible bug until a row and the header no longer line up.
|
|
303
|
+
"""
|
|
304
|
+
overhead = 6 + TIME_COLUMN
|
|
305
|
+
model_width = MODEL_COLUMN if width >= WIDE_PANEL else max(8, width // 5)
|
|
306
|
+
name_width = max(12, width - overhead - model_width)
|
|
307
|
+
return name_width, model_width, TIME_COLUMN
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def task_row(
|
|
311
|
+
task: SubagentTask,
|
|
312
|
+
*,
|
|
313
|
+
width: int,
|
|
314
|
+
now: float,
|
|
315
|
+
selected: bool = False,
|
|
316
|
+
) -> Text:
|
|
317
|
+
"""One table row: glyph, task name (with its error), model, time."""
|
|
318
|
+
name_width, model_width, _time_width = column_widths(width)
|
|
319
|
+
name = task.label or task.description or task.subagent_type or task.task_id
|
|
320
|
+
# Producers' labels already open with a kind prefix ("research: docling",
|
|
321
|
+
# "tests: pkg/x"), and a matching subagent type on top of one reads as a
|
|
322
|
+
# duplicate: "researcher: research: docling-0". So the type is only added to a
|
|
323
|
+
# label that does not carry a prefix of its own.
|
|
324
|
+
if task.subagent_type and not _has_kind_prefix(name):
|
|
325
|
+
name = f"{task.subagent_type}: {name}"
|
|
326
|
+
if task.status == "stopped":
|
|
327
|
+
name = f"stopped: {name}"
|
|
328
|
+
elif task.status == "failed" and task.error:
|
|
329
|
+
# The failure text is the whole point of the row, so it gets the space
|
|
330
|
+
# left over rather than the name being truncated to make room for it.
|
|
331
|
+
name = f"{name} — {task.error}"
|
|
332
|
+
if task.compacting:
|
|
333
|
+
name = f"⟳ compacting · {name}"
|
|
334
|
+
|
|
335
|
+
style = {"done": "dim", "failed": "red", "running": "", "stopped": "yellow"}.get(
|
|
336
|
+
task.status, ""
|
|
337
|
+
)
|
|
338
|
+
row = Text(style=style)
|
|
339
|
+
row.append(
|
|
340
|
+
"● " if task.status == "running" else status_glyph(task.status) + " ",
|
|
341
|
+
style="cyan" if task.status == "running" else style,
|
|
342
|
+
)
|
|
343
|
+
row.append(_fit(name, name_width))
|
|
344
|
+
row.append(" ")
|
|
345
|
+
row.append(_fit(task.model or "—", model_width), style="dim")
|
|
346
|
+
row.append(" ")
|
|
347
|
+
row.append(format_elapsed(elapsed_for(task, now)).rjust(TIME_COLUMN), style="dim")
|
|
348
|
+
if selected:
|
|
349
|
+
row.stylize("reverse")
|
|
350
|
+
return row
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def phase_row(
|
|
354
|
+
label: str,
|
|
355
|
+
tasks: list[SubagentTask],
|
|
356
|
+
*,
|
|
357
|
+
now: float,
|
|
358
|
+
expanded: bool = True,
|
|
359
|
+
selected: bool = False,
|
|
360
|
+
) -> Text:
|
|
361
|
+
"""One entry in the phases pane: caret, label, progress, elapsed."""
|
|
362
|
+
done, total, _phases, _failed = phase_summary(tasks)
|
|
363
|
+
row = Text()
|
|
364
|
+
row.append("▾ " if expanded else "▸ ")
|
|
365
|
+
row.append(f"{label} ", style="bold")
|
|
366
|
+
row.append(f"{done}/{total}", style="dim")
|
|
367
|
+
row.append(" · ", style="dim")
|
|
368
|
+
row.append(format_elapsed(sum(elapsed_for(task, now) for task in tasks)), style="dim")
|
|
369
|
+
if selected:
|
|
370
|
+
row.stylize("reverse")
|
|
371
|
+
return row
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def table_header(width: int) -> Text:
|
|
375
|
+
"""The ``TASK / MODEL / TIME`` header, aligned with :func:`task_row`.
|
|
376
|
+
|
|
377
|
+
Indented two cells to sit over the name column, since a task row spends its
|
|
378
|
+
first two on the status glyph and a space.
|
|
379
|
+
"""
|
|
380
|
+
name_width, model_width, _time_width = column_widths(width)
|
|
381
|
+
header = Text(style="dim")
|
|
382
|
+
header.append(" ")
|
|
383
|
+
header.append("TASK".ljust(name_width))
|
|
384
|
+
header.append(" ")
|
|
385
|
+
header.append("MODEL".ljust(model_width))
|
|
386
|
+
header.append(" ")
|
|
387
|
+
header.append("TIME".rjust(TIME_COLUMN))
|
|
388
|
+
return header
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@dataclass
|
|
392
|
+
class PanelBody:
|
|
393
|
+
"""A rendered panel, plus what each line belongs to.
|
|
394
|
+
|
|
395
|
+
The mapping is what makes the panel clickable: a click arrives as a line
|
|
396
|
+
offset, so the widget needs to know whether that line is a phase (toggle it)
|
|
397
|
+
or a task (select it).
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
lines: list[Text]
|
|
401
|
+
row_phases: list[str | None]
|
|
402
|
+
row_tasks: list[str | None]
|
|
403
|
+
#: The cell the task table starts at: a click left of it is on the phases pane.
|
|
404
|
+
task_column: int = 0
|
|
405
|
+
|
|
406
|
+
def phase_at(self, index: int) -> str | None:
|
|
407
|
+
"""The phase for line *index*, or ``None`` if it is not a phase row."""
|
|
408
|
+
if 0 <= index < len(self.row_phases):
|
|
409
|
+
return self.row_phases[index]
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
def task_at(self, index: int) -> str | None:
|
|
413
|
+
"""The task id for line *index*, or ``None``."""
|
|
414
|
+
if 0 <= index < len(self.row_tasks):
|
|
415
|
+
return self.row_tasks[index]
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def panel_body(
|
|
420
|
+
tasks: list[SubagentTask],
|
|
421
|
+
*,
|
|
422
|
+
width: int,
|
|
423
|
+
now: float,
|
|
424
|
+
phase_order: list[str] | None = None,
|
|
425
|
+
collapsed_phases: frozenset[str] | set[str] = frozenset(),
|
|
426
|
+
selected: str | None = None,
|
|
427
|
+
) -> PanelBody:
|
|
428
|
+
"""The whole panel body: the phases pane beside the task table.
|
|
429
|
+
|
|
430
|
+
Two panes, not one list, because they answer different questions: the left
|
|
431
|
+
says which fan-out a task belongs to and how that fan-out is doing, the right
|
|
432
|
+
is the per-task detail. Below ~60 cells the pane is dropped so the table keeps
|
|
433
|
+
its columns rather than both being squeezed.
|
|
434
|
+
"""
|
|
435
|
+
left_width = PHASE_COLUMN if width >= TWO_PANE_WIDTH else 0
|
|
436
|
+
right_width = width - left_width - 1 if left_width else width
|
|
437
|
+
|
|
438
|
+
left: list[Text | None] = []
|
|
439
|
+
left_phases: list[str | None] = []
|
|
440
|
+
if left_width:
|
|
441
|
+
left.append(Text("Phases", style="dim"))
|
|
442
|
+
left_phases.append(None)
|
|
443
|
+
for position, phase_id in enumerate(_ordered_phases(tasks, phase_order), start=1):
|
|
444
|
+
members = [task for task in tasks if task.phase_id == phase_id]
|
|
445
|
+
collapsed = phase_id in collapsed_phases
|
|
446
|
+
left.append(
|
|
447
|
+
phase_row(
|
|
448
|
+
# Numbered, in arrival order: an eval call id's tail ("al_1")
|
|
449
|
+
# is not something to read a panel by.
|
|
450
|
+
"async" if phase_id == ASYNC_PHASE else str(position),
|
|
451
|
+
members,
|
|
452
|
+
now=now,
|
|
453
|
+
expanded=not collapsed,
|
|
454
|
+
selected=phase_id == selected,
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
left_phases.append(phase_id)
|
|
458
|
+
|
|
459
|
+
right: list[Text] = [table_header(right_width)]
|
|
460
|
+
right_tasks: list[str | None] = [None]
|
|
461
|
+
for task in tasks:
|
|
462
|
+
if task.phase_id in collapsed_phases:
|
|
463
|
+
continue
|
|
464
|
+
right.append(task_row(task, width=right_width, now=now, selected=task.task_id == selected))
|
|
465
|
+
right_tasks.append(task.task_id)
|
|
466
|
+
|
|
467
|
+
lines: list[Text] = []
|
|
468
|
+
row_phases: list[str | None] = []
|
|
469
|
+
row_tasks: list[str | None] = []
|
|
470
|
+
for index in range(max(len(left), len(right))):
|
|
471
|
+
line = Text()
|
|
472
|
+
if left_width:
|
|
473
|
+
pane = left[index] if index < len(left) else None
|
|
474
|
+
# A line holds a phase and a task side by side; `task_column` tells
|
|
475
|
+
# a click which of the two it landed on.
|
|
476
|
+
row_phases.append(left_phases[index] if index < len(left_phases) else None)
|
|
477
|
+
row_tasks.append(right_tasks[index] if index < len(right_tasks) else None)
|
|
478
|
+
line.append_text(_pad(pane, left_width))
|
|
479
|
+
line.append("│", style="dim")
|
|
480
|
+
else:
|
|
481
|
+
row_phases.append(None)
|
|
482
|
+
row_tasks.append(right_tasks[index] if index < len(right_tasks) else None)
|
|
483
|
+
if index < len(right):
|
|
484
|
+
line.append_text(right[index])
|
|
485
|
+
lines.append(line)
|
|
486
|
+
return PanelBody(
|
|
487
|
+
lines=lines,
|
|
488
|
+
row_phases=row_phases,
|
|
489
|
+
row_tasks=row_tasks,
|
|
490
|
+
task_column=left_width + 1 if left_width else 0,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def panel_lines(
|
|
495
|
+
tasks: list[SubagentTask],
|
|
496
|
+
*,
|
|
497
|
+
width: int,
|
|
498
|
+
now: float,
|
|
499
|
+
phase_order: list[str] | None = None,
|
|
500
|
+
selected: str | None = None,
|
|
501
|
+
) -> list[Text]:
|
|
502
|
+
"""Just the rendered lines of :func:`panel_body`."""
|
|
503
|
+
return panel_body(tasks, width=width, now=now, phase_order=phase_order, selected=selected).lines
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
# ── preview: what one row is doing ───────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
#: One preview item: ``(kind, text, call_id)`` with kind "text" | "tool" |
|
|
509
|
+
#: "result" | "error". ``call_id`` ties a result to its tool call; "" for prose.
|
|
510
|
+
PreviewEntry = tuple[str, str, str]
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def append_preview(entries: list[PreviewEntry], kind: str, text: str, call_id: str = "") -> None:
|
|
514
|
+
"""Add one item, joining streamed text onto the text before it."""
|
|
515
|
+
if kind == "text" and entries and entries[-1][0] == "text":
|
|
516
|
+
entries[-1] = ("text", entries[-1][1] + text, "")
|
|
517
|
+
else:
|
|
518
|
+
entries.append((kind, text, call_id))
|
|
519
|
+
del entries[:-MAX_PREVIEW_ENTRIES]
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def preview_renderable(
|
|
523
|
+
entries: list[PreviewEntry],
|
|
524
|
+
*,
|
|
525
|
+
markdown: Any = None,
|
|
526
|
+
cache: dict[PreviewEntry, Any] | None = None,
|
|
527
|
+
label: Text | None = None,
|
|
528
|
+
palette: Any = None,
|
|
529
|
+
) -> Any:
|
|
530
|
+
"""The preview body, laid out like the main transcript.
|
|
531
|
+
|
|
532
|
+
Prose is a message: the agent's name (``label``) over rendered Markdown. A
|
|
533
|
+
run of tool calls is one block under an "N tool calls" heading, one line per
|
|
534
|
+
call -- a bullet coloured by outcome, the call, then its result after a
|
|
535
|
+
dash -- which is how the main page's tool group draws them. A result is
|
|
536
|
+
matched to its call by ``call_id``; one with no call to sit beside (its call
|
|
537
|
+
was trimmed, or it never had an id) is hung on a line of its own.
|
|
538
|
+
|
|
539
|
+
``markdown`` is the Markdown class to build prose with (the TUI passes its
|
|
540
|
+
caching one). ``cache`` keeps each prose renderable between calls: a preview
|
|
541
|
+
is rebuilt on every update, and only the entry still streaming has changed.
|
|
542
|
+
``palette`` is the TUI's theme palette; plain colour names stand in without.
|
|
543
|
+
"""
|
|
544
|
+
from itertools import groupby
|
|
545
|
+
|
|
546
|
+
from rich.console import Group
|
|
547
|
+
from rich.padding import Padding
|
|
548
|
+
|
|
549
|
+
if markdown is None:
|
|
550
|
+
from rich.markdown import Markdown as markdown # noqa: N813
|
|
551
|
+
|
|
552
|
+
ok = getattr(palette, "tool_ok", "green")
|
|
553
|
+
fail = getattr(palette, "tool_fail", "red")
|
|
554
|
+
pending = getattr(palette, "tool_pending", "yellow")
|
|
555
|
+
dim = getattr(palette, "dim", "dim")
|
|
556
|
+
|
|
557
|
+
outcome = {cid: (kind, text) for kind, text, cid in entries if cid and kind != "tool"}
|
|
558
|
+
called = {cid for kind, _, cid in entries if cid and kind == "tool"}
|
|
559
|
+
shown = [
|
|
560
|
+
e
|
|
561
|
+
for e in entries
|
|
562
|
+
# A result drawn on its call's line is not drawn again; blanks say nothing.
|
|
563
|
+
if (e[0] == "tool" or e[2] not in called) and e[1].strip()
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
blocks: list[Any] = []
|
|
567
|
+
for is_prose, run in groupby(shown, key=lambda e: e[0] == "text"):
|
|
568
|
+
if is_prose:
|
|
569
|
+
for entry in run:
|
|
570
|
+
body = cache.get(entry) if cache is not None else None
|
|
571
|
+
if body is None:
|
|
572
|
+
body = markdown(entry[1].strip())
|
|
573
|
+
if cache is not None:
|
|
574
|
+
cache[entry] = body
|
|
575
|
+
blocks.append(Group(label, body) if label is not None else body)
|
|
576
|
+
continue
|
|
577
|
+
lines: list[Text] = []
|
|
578
|
+
calls = 0
|
|
579
|
+
for kind, text, cid in run:
|
|
580
|
+
if kind != "tool":
|
|
581
|
+
style = fail if kind == "error" else dim
|
|
582
|
+
lines.append(Text.assemble((" ⎿ ", dim), (text.strip(), style)))
|
|
583
|
+
continue
|
|
584
|
+
calls += 1
|
|
585
|
+
result, detail = outcome.get(cid, ("", ""))
|
|
586
|
+
mark = fail if result == "error" else ok if result else pending if cid else dim
|
|
587
|
+
style = fail if result == "error" else dim
|
|
588
|
+
line = Text.assemble(("• ", f"bold {mark}"), (text.strip(), style))
|
|
589
|
+
# The summary's own ✓/✗ repeats what the bullet's colour already says.
|
|
590
|
+
if detail := detail.strip().lstrip("✓✗ "):
|
|
591
|
+
line.append(f" — {detail}", style=style)
|
|
592
|
+
lines.append(line)
|
|
593
|
+
if calls:
|
|
594
|
+
lines.insert(0, Text(f"{calls} tool call{'' if calls == 1 else 's'}", style="bold"))
|
|
595
|
+
blocks.append(Group(*lines))
|
|
596
|
+
if cache is not None and len(cache) > 2 * MAX_PREVIEW_ENTRIES:
|
|
597
|
+
cache.clear() # superseded streaming prefixes; the next call refills it
|
|
598
|
+
|
|
599
|
+
parts: list[Any] = []
|
|
600
|
+
for block in blocks:
|
|
601
|
+
if parts:
|
|
602
|
+
parts.append(Text(""))
|
|
603
|
+
# The transcript's own inset (ChatMessage and the tool group pad by 2).
|
|
604
|
+
parts.append(Padding(block, (0, 2)))
|
|
605
|
+
return Group(*parts)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def remote_preview(messages: object) -> list[PreviewEntry]:
|
|
609
|
+
"""Preview entries from an async agent's thread messages.
|
|
610
|
+
|
|
611
|
+
They arrive from the server as plain dicts, and only as whole messages, so
|
|
612
|
+
this is rebuilt from the full list on every poll rather than appended to.
|
|
613
|
+
"""
|
|
614
|
+
from novacode_cli.ui.ui_elements import format_tool_display, format_tool_result_preview
|
|
615
|
+
|
|
616
|
+
entries: list[PreviewEntry] = []
|
|
617
|
+
for message in messages if isinstance(messages, list) else []:
|
|
618
|
+
if not isinstance(message, dict):
|
|
619
|
+
continue
|
|
620
|
+
content = message.get("content")
|
|
621
|
+
if isinstance(content, list):
|
|
622
|
+
content = "".join(
|
|
623
|
+
part if isinstance(part, str) else str(part.get("text") or "")
|
|
624
|
+
for part in content
|
|
625
|
+
if isinstance(part, (str, dict))
|
|
626
|
+
)
|
|
627
|
+
content = str(content or "")
|
|
628
|
+
kind = message.get("type")
|
|
629
|
+
if kind == "human":
|
|
630
|
+
entries.append(("result", f"task: {content}", ""))
|
|
631
|
+
elif kind == "ai":
|
|
632
|
+
if content.strip():
|
|
633
|
+
entries.append(("text", content, ""))
|
|
634
|
+
for call in message.get("tool_calls") or []:
|
|
635
|
+
if isinstance(call, dict):
|
|
636
|
+
name = str(call.get("name") or "tool")
|
|
637
|
+
args = call.get("args")
|
|
638
|
+
entries.append(
|
|
639
|
+
(
|
|
640
|
+
"tool",
|
|
641
|
+
format_tool_display(name, args if isinstance(args, dict) else {}),
|
|
642
|
+
str(call.get("id") or ""),
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
elif kind == "tool":
|
|
646
|
+
status = str(message.get("status") or "success")
|
|
647
|
+
line = format_tool_result_preview(str(message.get("name") or ""), content, status) or ""
|
|
648
|
+
# Kept even when it has no summary (file tools): it is what marks
|
|
649
|
+
# the call beside it as finished.
|
|
650
|
+
failed = status != "success" or line.startswith("✗")
|
|
651
|
+
entries.append(
|
|
652
|
+
("error" if failed else "result", line, str(message.get("tool_call_id") or ""))
|
|
653
|
+
)
|
|
654
|
+
return entries[-MAX_PREVIEW_ENTRIES:]
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _ordered_phases(tasks: list[SubagentTask], phase_order: list[str] | None) -> list[str]:
|
|
658
|
+
"""Phase ids in the order they first appeared, or the caller's order."""
|
|
659
|
+
if phase_order is not None:
|
|
660
|
+
return [phase_id for phase_id in phase_order if phase_id][:MAX_PHASES]
|
|
661
|
+
# dict.fromkeys, not a set: the order a fan-out's phases arrived in is the
|
|
662
|
+
# order the user watched them start.
|
|
663
|
+
return list(dict.fromkeys(task.phase_id for task in tasks if task.phase_id))[:MAX_PHASES]
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _pad(text: Text | None, width: int) -> Text:
|
|
667
|
+
"""Exactly *width* cells: padded, or ellipsised if the text overruns."""
|
|
668
|
+
if text is None:
|
|
669
|
+
return Text(" " * width)
|
|
670
|
+
plain = text.plain
|
|
671
|
+
if len(plain) == width:
|
|
672
|
+
return text
|
|
673
|
+
if len(plain) < width:
|
|
674
|
+
padded = Text()
|
|
675
|
+
padded.append_text(text)
|
|
676
|
+
padded.append(" " * (width - len(plain)))
|
|
677
|
+
return padded
|
|
678
|
+
clipped = Text()
|
|
679
|
+
clipped.append_text(text)
|
|
680
|
+
clipped.truncate(width - 1, overflow="ellipsis")
|
|
681
|
+
return clipped
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _has_kind_prefix(name: str) -> bool:
|
|
685
|
+
"""Whether a label already opens with its own ``kind:`` prefix."""
|
|
686
|
+
return name.split(maxsplit=1)[0].endswith(":")
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _fit(text: str, width: int) -> str:
|
|
690
|
+
"""Pad or ellipsise to exactly *width* cells (single-line)."""
|
|
691
|
+
flat = " ".join(str(text).split())
|
|
692
|
+
if len(flat) <= width:
|
|
693
|
+
return flat.ljust(width)
|
|
694
|
+
if width <= 1:
|
|
695
|
+
return "…"[:width]
|
|
696
|
+
return flat[: width - 1] + "…"
|