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.
Files changed (691) hide show
  1. novacode_cli/__init__.py +29 -0
  2. novacode_cli/__main__.py +6 -0
  3. novacode_cli/_lazy_heavy.py +99 -0
  4. novacode_cli/_version.py +3 -0
  5. novacode_cli/agent_stream.py +178 -0
  6. novacode_cli/agents/agent_file.py +250 -0
  7. novacode_cli/agents/agent_mailbox.py +120 -0
  8. novacode_cli/agents/approval_decisions.py +146 -0
  9. novacode_cli/agents/arg_repair.py +196 -0
  10. novacode_cli/agents/async_agents/__init__.py +6 -0
  11. novacode_cli/agents/async_agents/_config.py +213 -0
  12. novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
  13. novacode_cli/agents/async_agents/_specialist.py +84 -0
  14. novacode_cli/agents/async_agents/_test_commands.py +89 -0
  15. novacode_cli/agents/async_agents/_tools.py +432 -0
  16. novacode_cli/agents/async_agents/general_purpose.py +42 -0
  17. novacode_cli/agents/async_agents/langgraph.json +9 -0
  18. novacode_cli/agents/async_agents/research.py +22 -0
  19. novacode_cli/agents/async_context.py +303 -0
  20. novacode_cli/agents/async_on_demand.py +52 -0
  21. novacode_cli/agents/async_task_errors.py +75 -0
  22. novacode_cli/agents/async_workspace.py +27 -0
  23. novacode_cli/agents/attribution.py +165 -0
  24. novacode_cli/agents/browser_mode.py +119 -0
  25. novacode_cli/agents/btw_agent.py +71 -0
  26. novacode_cli/agents/caveman.py +352 -0
  27. novacode_cli/agents/caveman_support.py +370 -0
  28. novacode_cli/agents/code_mode.py +774 -0
  29. novacode_cli/agents/code_runtime.py +160 -0
  30. novacode_cli/agents/context_compaction.py +1339 -0
  31. novacode_cli/agents/core_agent.py +2710 -0
  32. novacode_cli/agents/default_subagents/__init__.py +24 -0
  33. novacode_cli/agents/default_subagents/async_subagents.py +296 -0
  34. novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
  35. novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
  36. novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
  37. novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
  38. novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
  39. novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
  40. novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
  41. novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
  42. novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
  43. novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
  44. novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
  45. novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
  46. novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
  47. novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
  48. novacode_cli/agents/default_subagents/prompt.py +88 -0
  49. novacode_cli/agents/default_subagents/subagents.py +138 -0
  50. novacode_cli/agents/dream_agent.py +93 -0
  51. novacode_cli/agents/edit_diagnostics.py +362 -0
  52. novacode_cli/agents/edit_fallback.py +62 -0
  53. novacode_cli/agents/handoff.py +166 -0
  54. novacode_cli/agents/invocation.py +152 -0
  55. novacode_cli/agents/model_budget.py +42 -0
  56. novacode_cli/agents/model_retry.py +113 -0
  57. novacode_cli/agents/model_router.py +420 -0
  58. novacode_cli/agents/openai_decisions.py +186 -0
  59. novacode_cli/agents/plan_agent/__init__.py +12 -0
  60. novacode_cli/agents/plan_agent/plan_agent.py +357 -0
  61. novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
  62. novacode_cli/agents/plan_execution.py +237 -0
  63. novacode_cli/agents/ponytail.py +66 -0
  64. novacode_cli/agents/prime.py +274 -0
  65. novacode_cli/agents/provider_caching.py +99 -0
  66. novacode_cli/agents/server_launcher.py +904 -0
  67. novacode_cli/agents/strict_tools.py +384 -0
  68. novacode_cli/agents/task_discipline.py +294 -0
  69. novacode_cli/agents/tool_call_repair.py +290 -0
  70. novacode_cli/agents/tool_offload.py +291 -0
  71. novacode_cli/agents/tool_search.py +806 -0
  72. novacode_cli/agents/tool_search_variants.py +106 -0
  73. novacode_cli/agents/tool_verdicts.py +1194 -0
  74. novacode_cli/agents/user_async_agents.py +312 -0
  75. novacode_cli/artifacts/__init__.py +10 -0
  76. novacode_cli/artifacts/registry.py +278 -0
  77. novacode_cli/artifacts/server.py +426 -0
  78. novacode_cli/assets/Nova.png +0 -0
  79. novacode_cli/assets/nova-avatar.png +0 -0
  80. novacode_cli/audio/__init__.py +301 -0
  81. novacode_cli/audio/capture.py +137 -0
  82. novacode_cli/audio/downloads.py +273 -0
  83. novacode_cli/audio/pipeline.py +391 -0
  84. novacode_cli/audio/playback.py +87 -0
  85. novacode_cli/audio/providers.py +208 -0
  86. novacode_cli/audio/speakable.py +78 -0
  87. novacode_cli/audio/stt.py +132 -0
  88. novacode_cli/audio/stt_deepgram.py +69 -0
  89. novacode_cli/audio/stt_elevenlabs.py +89 -0
  90. novacode_cli/audio/stt_parakeet.py +94 -0
  91. novacode_cli/audio/stt_whistle.py +61 -0
  92. novacode_cli/audio/summarize.py +92 -0
  93. novacode_cli/audio/tts.py +85 -0
  94. novacode_cli/audio/tts_kokoro.py +73 -0
  95. novacode_cli/audio/tts_orpheus.py +180 -0
  96. novacode_cli/audio/tts_pocket.py +131 -0
  97. novacode_cli/audio/tts_sano.py +69 -0
  98. novacode_cli/audio/vad.py +145 -0
  99. novacode_cli/backends/__init__.py +17 -0
  100. novacode_cli/backends/filesystem.py +894 -0
  101. novacode_cli/backends/native_search.py +92 -0
  102. novacode_cli/bootstrap/__init__.py +9 -0
  103. novacode_cli/bootstrap/env_snapshot.py +240 -0
  104. novacode_cli/bootstrap/steering.py +464 -0
  105. novacode_cli/bootstrap/vision_router.py +604 -0
  106. novacode_cli/brand.py +362 -0
  107. novacode_cli/cli_args.py +718 -0
  108. novacode_cli/commands/__init__.py +237 -0
  109. novacode_cli/commands/agents_commands.py +231 -0
  110. novacode_cli/commands/auth_handler.py +40 -0
  111. novacode_cli/commands/browser_use_handler.py +75 -0
  112. novacode_cli/commands/cache_handler.py +32 -0
  113. novacode_cli/commands/chat_handler.py +1357 -0
  114. novacode_cli/commands/ci_fix.py +171 -0
  115. novacode_cli/commands/claude_plan.py +331 -0
  116. novacode_cli/commands/codex_plan.py +195 -0
  117. novacode_cli/commands/commands.py +1113 -0
  118. novacode_cli/commands/create_handler.py +124 -0
  119. novacode_cli/commands/create_server.py +1887 -0
  120. novacode_cli/commands/cron_handler.py +119 -0
  121. novacode_cli/commands/dream_handler.py +203 -0
  122. novacode_cli/commands/effort_handler.py +65 -0
  123. novacode_cli/commands/evolution_handler.py +214 -0
  124. novacode_cli/commands/file_commands.py +381 -0
  125. novacode_cli/commands/handoff_handler.py +68 -0
  126. novacode_cli/commands/hooks_handler.py +269 -0
  127. novacode_cli/commands/init_handler.py +146 -0
  128. novacode_cli/commands/learning_handler.py +160 -0
  129. novacode_cli/commands/lite_mode_handler.py +76 -0
  130. novacode_cli/commands/log_commands.py +363 -0
  131. novacode_cli/commands/mcp_handler.py +29 -0
  132. novacode_cli/commands/model_handler.py +29 -0
  133. novacode_cli/commands/notifications_handler.py +158 -0
  134. novacode_cli/commands/onboard_handler.py +121 -0
  135. novacode_cli/commands/plan_handler.py +440 -0
  136. novacode_cli/commands/planner_usage.py +250 -0
  137. novacode_cli/commands/plugin_install_handler.py +38 -0
  138. novacode_cli/commands/plugins_handler.py +35 -0
  139. novacode_cli/commands/prime_handler.py +455 -0
  140. novacode_cli/commands/prompt_handler.py +149 -0
  141. novacode_cli/commands/provider_usage.py +160 -0
  142. novacode_cli/commands/ralph_events.py +106 -0
  143. novacode_cli/commands/ralph_handler.py +1293 -0
  144. novacode_cli/commands/refine_handler.py +213 -0
  145. novacode_cli/commands/research_handler.py +202 -0
  146. novacode_cli/commands/rootapp_remote.py +83 -0
  147. novacode_cli/commands/router_handler.py +36 -0
  148. novacode_cli/commands/server_commands.py +249 -0
  149. novacode_cli/commands/session_commands.py +203 -0
  150. novacode_cli/commands/share_handler.py +450 -0
  151. novacode_cli/commands/side_commands.py +366 -0
  152. novacode_cli/commands/skill_invoke.py +420 -0
  153. novacode_cli/commands/skills_commands.py +48 -0
  154. novacode_cli/commands/trace_handler.py +322 -0
  155. novacode_cli/commands/trello_handler.py +226 -0
  156. novacode_cli/commands/trello_remote.py +177 -0
  157. novacode_cli/commands/trello_server.py +1214 -0
  158. novacode_cli/commands/vision_handler.py +127 -0
  159. novacode_cli/commands/voice_handler.py +447 -0
  160. novacode_cli/commands/webhook_handler.py +134 -0
  161. novacode_cli/commands/wiki_commands.py +227 -0
  162. novacode_cli/compaction.py +753 -0
  163. novacode_cli/computation_cache.py +159 -0
  164. novacode_cli/computer/__init__.py +1 -0
  165. novacode_cli/computer/persistent.py +234 -0
  166. novacode_cli/computer/preview.py +253 -0
  167. novacode_cli/computer/runtime.py +474 -0
  168. novacode_cli/computer/windows_console.py +152 -0
  169. novacode_cli/computer/windows_qemu.py +211 -0
  170. novacode_cli/config/browser_mode.py +159 -0
  171. novacode_cli/config/catalog_models.py +95 -0
  172. novacode_cli/config/config.py +1388 -0
  173. novacode_cli/config/credentials.py +540 -0
  174. novacode_cli/config/google_oauth_auth.py +281 -0
  175. novacode_cli/config/model_capabilities.py +343 -0
  176. novacode_cli/config/model_catalog.py +275 -0
  177. novacode_cli/config/model_create.py +1325 -0
  178. novacode_cli/config/model_manager.py +695 -0
  179. novacode_cli/config/nova_config.py +1283 -0
  180. novacode_cli/config/openai_chatgpt_auth.py +384 -0
  181. novacode_cli/config/opencode_gateway.py +25 -0
  182. novacode_cli/config/plan_mode.py +188 -0
  183. novacode_cli/config/provider_auth.py +438 -0
  184. novacode_cli/config/provider_catalog.json +5127 -0
  185. novacode_cli/config/provider_registry.py +38 -0
  186. novacode_cli/config/provider_transports.py +258 -0
  187. novacode_cli/config/role_models.py +175 -0
  188. novacode_cli/context/__init__.py +50 -0
  189. novacode_cli/context/_analysis.py +799 -0
  190. novacode_cli/context/_budget.py +116 -0
  191. novacode_cli/context/_conversation.py +73 -0
  192. novacode_cli/context/_dynamic.py +264 -0
  193. novacode_cli/context/_model_config.py +380 -0
  194. novacode_cli/context/_models_dev.py +99 -0
  195. novacode_cli/context/archives.py +248 -0
  196. novacode_cli/context/estimates.py +65 -0
  197. novacode_cli/context/history.py +17 -0
  198. novacode_cli/context/knowledge_cache.py +292 -0
  199. novacode_cli/context/manager.py +101 -0
  200. novacode_cli/context/observations.py +65 -0
  201. novacode_cli/context/pressure.py +168 -0
  202. novacode_cli/context/provider_accounting.py +109 -0
  203. novacode_cli/context/snapshot.py +493 -0
  204. novacode_cli/context/strategies.py +279 -0
  205. novacode_cli/context/task_progress.py +122 -0
  206. novacode_cli/core/__init__.py +0 -0
  207. novacode_cli/core/agent_loop.py +1821 -0
  208. novacode_cli/core/autonomous_loop.py +277 -0
  209. novacode_cli/core/code_approvals.py +57 -0
  210. novacode_cli/core/input_preparation.py +276 -0
  211. novacode_cli/core/reasoning.py +67 -0
  212. novacode_cli/core/streaming.py +326 -0
  213. novacode_cli/core/subagent_tasks.py +696 -0
  214. novacode_cli/core/subagent_tracking.py +235 -0
  215. novacode_cli/core/tool_results.py +116 -0
  216. novacode_cli/core/verification_loop.py +272 -0
  217. novacode_cli/council.py +595 -0
  218. novacode_cli/council_consensus.py +115 -0
  219. novacode_cli/council_decision.py +157 -0
  220. novacode_cli/council_planner.py +1421 -0
  221. novacode_cli/council_settings.py +208 -0
  222. novacode_cli/council_web_settings.py +41 -0
  223. novacode_cli/council_workspace.py +56 -0
  224. novacode_cli/cowork/__init__.py +10 -0
  225. novacode_cli/cowork/broker_middleware.py +162 -0
  226. novacode_cli/cowork/launcher.py +102 -0
  227. novacode_cli/cowork/policy.py +284 -0
  228. novacode_cli/cowork/ui.py +826 -0
  229. novacode_cli/daemons/__init__.py +1 -0
  230. novacode_cli/daemons/registry.py +342 -0
  231. novacode_cli/desktop_notifications.py +302 -0
  232. novacode_cli/doctor.py +251 -0
  233. novacode_cli/embeddings.py +109 -0
  234. novacode_cli/entrypoint.py +56 -0
  235. novacode_cli/errors/__init__.py +19 -0
  236. novacode_cli/errors/handlers.py +648 -0
  237. novacode_cli/errors/provider_errors.py +249 -0
  238. novacode_cli/errors/taxonomy.py +80 -0
  239. novacode_cli/events.py +179 -0
  240. novacode_cli/file_ops.py +714 -0
  241. novacode_cli/git_safety.py +281 -0
  242. novacode_cli/handoff.py +383 -0
  243. novacode_cli/headless/__init__.py +18 -0
  244. novacode_cli/headless/input.py +58 -0
  245. novacode_cli/headless/output.py +224 -0
  246. novacode_cli/headless/pipe.py +532 -0
  247. novacode_cli/headless/runner.py +461 -0
  248. novacode_cli/hermes/__init__.py +39 -0
  249. novacode_cli/hermes/activation.py +297 -0
  250. novacode_cli/hermes/config.py +129 -0
  251. novacode_cli/hermes/curator.py +199 -0
  252. novacode_cli/hermes/evolution.py +374 -0
  253. novacode_cli/hermes/ledger.py +364 -0
  254. novacode_cli/hermes/memory_tiers.py +969 -0
  255. novacode_cli/hermes/middleware.py +678 -0
  256. novacode_cli/hermes/overview.py +174 -0
  257. novacode_cli/hermes/prompt_evolution.py +503 -0
  258. novacode_cli/hermes/refine_loop.py +643 -0
  259. novacode_cli/hermes/refinement_log.py +190 -0
  260. novacode_cli/hermes/review.py +674 -0
  261. novacode_cli/hermes/skill_debate.py +135 -0
  262. novacode_cli/hermes/skill_discovery.py +450 -0
  263. novacode_cli/hermes/skill_manager.py +335 -0
  264. novacode_cli/hermes/skill_proposals.py +43 -0
  265. novacode_cli/hermes/test_gaming.py +119 -0
  266. novacode_cli/hermes/tracker.py +435 -0
  267. novacode_cli/hermes/tuner.py +206 -0
  268. novacode_cli/hermes/verification_memory.py +134 -0
  269. novacode_cli/hermes/verifier.py +277 -0
  270. novacode_cli/hitl/__init__.py +22 -0
  271. novacode_cli/hitl/interrupts.py +493 -0
  272. novacode_cli/hooks.py +359 -0
  273. novacode_cli/image_utils.py +517 -0
  274. novacode_cli/init/__init__.py +7 -0
  275. novacode_cli/init/events.py +43 -0
  276. novacode_cli/input_utils.py +526 -0
  277. novacode_cli/install_commands.py +87 -0
  278. novacode_cli/integrations/__init__.py +82 -0
  279. novacode_cli/integrations/daytona.py +122 -0
  280. novacode_cli/integrations/docker.py +309 -0
  281. novacode_cli/integrations/host_path.py +111 -0
  282. novacode_cli/integrations/langsmith.py +151 -0
  283. novacode_cli/integrations/modal.py +134 -0
  284. novacode_cli/integrations/runloop.py +130 -0
  285. novacode_cli/integrations/sandbox_factory.py +1388 -0
  286. novacode_cli/integrations/sandbox_registry.py +312 -0
  287. novacode_cli/integrations/workdir_backend.py +589 -0
  288. novacode_cli/main.py +2078 -0
  289. novacode_cli/mcp/__init__.py +73 -0
  290. novacode_cli/mcp/auth.py +189 -0
  291. novacode_cli/mcp/client.py +571 -0
  292. novacode_cli/mcp/code_mode.py +5 -0
  293. novacode_cli/mcp/code_runtime.py +5 -0
  294. novacode_cli/mcp/commands.py +902 -0
  295. novacode_cli/mcp/config.py +406 -0
  296. novacode_cli/mcp/generic.py +128 -0
  297. novacode_cli/mcp/middleware.py +993 -0
  298. novacode_cli/mcp/presets.py +367 -0
  299. novacode_cli/memory/__init__.py +1 -0
  300. novacode_cli/memory/agent_memory.py +1312 -0
  301. novacode_cli/memory/builtin_lessons.py +60 -0
  302. novacode_cli/memory/dream_gates.py +192 -0
  303. novacode_cli/memory/limits.py +59 -0
  304. novacode_cli/memory/parsed_cache.py +106 -0
  305. novacode_cli/memory/recall_stats.py +97 -0
  306. novacode_cli/memory/semantic.py +116 -0
  307. novacode_cli/memory/store.py +335 -0
  308. novacode_cli/memory/wiki.py +274 -0
  309. novacode_cli/migrate.py +185 -0
  310. novacode_cli/onboarding.py +649 -0
  311. novacode_cli/onboarding_check.py +119 -0
  312. novacode_cli/path_approval.py +297 -0
  313. novacode_cli/plan_archive.py +148 -0
  314. novacode_cli/plugins/__init__.py +121 -0
  315. novacode_cli/plugins/claude_plugins.py +538 -0
  316. novacode_cli/plugins/cli.py +168 -0
  317. novacode_cli/plugins/components.py +413 -0
  318. novacode_cli/plugins/hooks.py +356 -0
  319. novacode_cli/plugins/loader.py +317 -0
  320. novacode_cli/plugins/marketplaces.py +199 -0
  321. novacode_cli/plugins/storage.py +70 -0
  322. novacode_cli/prime.py +759 -0
  323. novacode_cli/prime_factory/__init__.py +15 -0
  324. novacode_cli/prime_factory/container.py +198 -0
  325. novacode_cli/prime_factory/controller.py +565 -0
  326. novacode_cli/prime_factory/knowledge.py +261 -0
  327. novacode_cli/prime_factory/publication.py +218 -0
  328. novacode_cli/prime_factory/recovery.py +50 -0
  329. novacode_cli/prime_factory/research.py +370 -0
  330. novacode_cli/prime_factory/runner.py +608 -0
  331. novacode_cli/prime_factory/runtime.py +261 -0
  332. novacode_cli/prime_factory/state.py +201 -0
  333. novacode_cli/prime_factory/store.py +128 -0
  334. novacode_cli/prime_factory/worker.py +129 -0
  335. novacode_cli/prime_factory/workflow.py +899 -0
  336. novacode_cli/prime_factory/workspace.py +317 -0
  337. novacode_cli/prime_memory.py +290 -0
  338. novacode_cli/process_manager.py +585 -0
  339. novacode_cli/project_onboard.py +537 -0
  340. novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
  341. novacode_cli/prompts/__init__.py +168 -0
  342. novacode_cli/prompts/agent_definitions.jinja +11 -0
  343. novacode_cli/prompts/agent_generation.jinja +125 -0
  344. novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
  345. novacode_cli/prompts/claude_plan_mode.jinja +12 -0
  346. novacode_cli/prompts/coding_workflow.jinja +18 -0
  347. novacode_cli/prompts/computer_use.jinja +17 -0
  348. novacode_cli/prompts/core_agent_system.jinja +81 -0
  349. novacode_cli/prompts/delegation.jinja +15 -0
  350. novacode_cli/prompts/desktop_power.jinja +4 -0
  351. novacode_cli/prompts/file_paths.jinja +15 -0
  352. novacode_cli/prompts/file_tracker.jinja +23 -0
  353. novacode_cli/prompts/init_agents_md.jinja +56 -0
  354. novacode_cli/prompts/init_exploration.jinja +174 -0
  355. novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
  356. novacode_cli/prompts/instruction_scope.jinja +5 -0
  357. novacode_cli/prompts/learnings_extraction.jinja +45 -0
  358. novacode_cli/prompts/longterm_memory.jinja +54 -0
  359. novacode_cli/prompts/mcp.jinja +84 -0
  360. novacode_cli/prompts/memory_consolidation.jinja +40 -0
  361. novacode_cli/prompts/nova_evolution.jinja +73 -0
  362. novacode_cli/prompts/nova_review.jinja +158 -0
  363. novacode_cli/prompts/nova_skill_debate.jinja +36 -0
  364. novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
  365. novacode_cli/prompts/nova_verify.jinja +100 -0
  366. novacode_cli/prompts/onboard_dashboard.jinja +378 -0
  367. novacode_cli/prompts/plan_agent.jinja +87 -0
  368. novacode_cli/prompts/plan_scouts.jinja +5 -0
  369. novacode_cli/prompts/plan_storage.jinja +8 -0
  370. novacode_cli/prompts/planning.jinja +82 -0
  371. novacode_cli/prompts/ponytail/LICENSE +21 -0
  372. novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
  373. novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
  374. novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
  375. novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
  376. novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
  377. novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
  378. novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
  379. novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
  380. novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
  381. novacode_cli/prompts/prompt_evolution.jinja +27 -0
  382. novacode_cli/prompts/ralph_iteration.jinja +75 -0
  383. novacode_cli/prompts/refine_gate.jinja +31 -0
  384. novacode_cli/prompts/refine_plan.jinja +64 -0
  385. novacode_cli/prompts/refine_review.jinja +34 -0
  386. novacode_cli/prompts/research_delivery.jinja +9 -0
  387. novacode_cli/prompts/research_swarm.jinja +130 -0
  388. novacode_cli/prompts/response_style.jinja +5 -0
  389. novacode_cli/prompts/session_summarization.jinja +86 -0
  390. novacode_cli/prompts/skill_creation.jinja +85 -0
  391. novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
  392. novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
  393. novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
  394. novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
  395. novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
  396. novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
  397. novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
  398. novacode_cli/prompts/summarization.jinja +107 -0
  399. novacode_cli/prompts/summarization_answers.jinja +23 -0
  400. novacode_cli/prompts/summarization_questions.jinja +32 -0
  401. novacode_cli/prompts/vision_caption.jinja +21 -0
  402. novacode_cli/prompts/voice_summary.jinja +65 -0
  403. novacode_cli/prompts/wiki_file.jinja +57 -0
  404. novacode_cli/prompts/wiki_ingest.jinja +87 -0
  405. novacode_cli/recovery.py +383 -0
  406. novacode_cli/remote/__init__.py +45 -0
  407. novacode_cli/remote/approvals.py +282 -0
  408. novacode_cli/remote/async_task_clients.py +97 -0
  409. novacode_cli/remote/async_task_watcher.py +240 -0
  410. novacode_cli/remote/bridge.py +905 -0
  411. novacode_cli/remote/command_menu.py +26 -0
  412. novacode_cli/remote/command_ui.py +243 -0
  413. novacode_cli/remote/config.py +199 -0
  414. novacode_cli/remote/discord_bridge.py +530 -0
  415. novacode_cli/remote/discord_format.py +31 -0
  416. novacode_cli/remote/discord_threads.py +225 -0
  417. novacode_cli/remote/images.py +97 -0
  418. novacode_cli/remote/processor.py +601 -0
  419. novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
  420. novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
  421. novacode_cli/remote/rootapp_bot/package.json +18 -0
  422. novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
  423. novacode_cli/remote/rootapp_bridge.py +370 -0
  424. novacode_cli/remote/rootapp_setup.py +89 -0
  425. novacode_cli/remote/routing.py +103 -0
  426. novacode_cli/remote/scheduler.py +302 -0
  427. novacode_cli/remote/session.py +164 -0
  428. novacode_cli/remote/status.py +411 -0
  429. novacode_cli/remote/streaming.py +188 -0
  430. novacode_cli/remote/telegram_bridge.py +1118 -0
  431. novacode_cli/remote/telegram_format.py +215 -0
  432. novacode_cli/remote/telegram_hub.py +256 -0
  433. novacode_cli/remote/voice_notes.py +120 -0
  434. novacode_cli/remote/webhook_adapters.py +167 -0
  435. novacode_cli/remote/webhook_server.py +154 -0
  436. novacode_cli/rewind.py +150 -0
  437. novacode_cli/rooms/__init__.py +1 -0
  438. novacode_cli/rooms/bridge.py +86 -0
  439. novacode_cli/rooms/cli.py +231 -0
  440. novacode_cli/rooms/nova.py +227 -0
  441. novacode_cli/rooms/runtime.py +299 -0
  442. novacode_cli/rooms/store.py +538 -0
  443. novacode_cli/rooms/workspace.py +94 -0
  444. novacode_cli/security/__init__.py +29 -0
  445. novacode_cli/security/approval_context.py +45 -0
  446. novacode_cli/security/delegated_approval.py +68 -0
  447. novacode_cli/security/local_http.py +85 -0
  448. novacode_cli/security/middleware.py +87 -0
  449. novacode_cli/security/paths.py +35 -0
  450. novacode_cli/security/policy.py +535 -0
  451. novacode_cli/security/policy_writer.py +90 -0
  452. novacode_cli/security/prompt_injection.py +203 -0
  453. novacode_cli/security/remember.py +81 -0
  454. novacode_cli/security/rule_synthesis.py +116 -0
  455. novacode_cli/security/secret_files.py +61 -0
  456. novacode_cli/security/session_allow.py +114 -0
  457. novacode_cli/security/unicode_security.py +516 -0
  458. novacode_cli/security/validator.py +173 -0
  459. novacode_cli/server/__init__.py +9 -0
  460. novacode_cli/server/app.py +632 -0
  461. novacode_cli/server/event_adapter.py +189 -0
  462. novacode_cli/server/session_manager.py +90 -0
  463. novacode_cli/server_main.py +64 -0
  464. novacode_cli/server_runner/dev_server.py +530 -0
  465. novacode_cli/server_runner/test_runner.py +470 -0
  466. novacode_cli/session/__init__.py +1 -0
  467. novacode_cli/session/adapters/__init__.py +240 -0
  468. novacode_cli/session/adapters/claude_code.py +102 -0
  469. novacode_cli/session/adapters/codex.py +77 -0
  470. novacode_cli/session/adapters/harness_native.py +75 -0
  471. novacode_cli/session/checkpoint_coordinator.py +448 -0
  472. novacode_cli/session/context_restore.py +25 -0
  473. novacode_cli/session/history_export.py +228 -0
  474. novacode_cli/session/history_tree.py +174 -0
  475. novacode_cli/session/imported_context.py +267 -0
  476. novacode_cli/session/runtime_identity.py +32 -0
  477. novacode_cli/session/session_persistence.py +1168 -0
  478. novacode_cli/session/session_prompt_builder.py +279 -0
  479. novacode_cli/session/session_restore.py +351 -0
  480. novacode_cli/session/session_summarization.py +20 -0
  481. novacode_cli/session/transcript_journal.py +153 -0
  482. novacode_cli/sessions/__init__.py +11 -0
  483. novacode_cli/sessions/gateway.py +84 -0
  484. novacode_cli/sessions/launch.py +51 -0
  485. novacode_cli/sessions/lease.py +213 -0
  486. novacode_cli/sessions/messaging.py +255 -0
  487. novacode_cli/sessions/protocol.py +216 -0
  488. novacode_cli/sessions/resume.py +31 -0
  489. novacode_cli/sessions/supervisor.py +498 -0
  490. novacode_cli/sessions/tasks.py +73 -0
  491. novacode_cli/sessions/tui_messaging.py +413 -0
  492. novacode_cli/sessions/worker.py +652 -0
  493. novacode_cli/sessions/worktree.py +262 -0
  494. novacode_cli/shell/__init__.py +27 -0
  495. novacode_cli/shell/jobs.py +475 -0
  496. novacode_cli/shell/middleware.py +1568 -0
  497. novacode_cli/shell/os_sandbox.py +205 -0
  498. novacode_cli/shell/output.py +181 -0
  499. novacode_cli/shell/patterns.py +275 -0
  500. novacode_cli/shell/processes.py +87 -0
  501. novacode_cli/shell/utils.py +236 -0
  502. novacode_cli/skills/__init__.py +26 -0
  503. novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
  504. novacode_cli/skills/curation_middleware.py +69 -0
  505. novacode_cli/skills/executable.py +161 -0
  506. novacode_cli/skills/libraries.py +102 -0
  507. novacode_cli/skills/load.py +248 -0
  508. novacode_cli/skills/prune.py +215 -0
  509. novacode_cli/skills/refreshing_middleware.py +896 -0
  510. novacode_cli/skills/retrieval.py +248 -0
  511. novacode_cli/skills/runtime.py +118 -0
  512. novacode_cli/skills/schema.py +254 -0
  513. novacode_cli/skills/skill_creation.py +1549 -0
  514. novacode_cli/skills/skill_lock.py +75 -0
  515. novacode_cli/skills/skill_system_prompt.py +16 -0
  516. novacode_cli/skills/skills_prefs.py +126 -0
  517. novacode_cli/skills/tool_resolver.py +31 -0
  518. novacode_cli/skills/upstream.py +50 -0
  519. novacode_cli/skills/versioning.py +291 -0
  520. novacode_cli/states/Session.py +847 -0
  521. novacode_cli/states/__init__.py +31 -0
  522. novacode_cli/states/slices/__init__.py +17 -0
  523. novacode_cli/states/slices/agent_runtime.py +249 -0
  524. novacode_cli/states/slices/background_tasks.py +21 -0
  525. novacode_cli/states/slices/notifications.py +74 -0
  526. novacode_cli/states/slices/remote_bridge.py +56 -0
  527. novacode_cli/states/slices/ui_settings.py +49 -0
  528. novacode_cli/states/slices/wiki.py +25 -0
  529. novacode_cli/teammates/__init__.py +1 -0
  530. novacode_cli/teammates/controller.py +557 -0
  531. novacode_cli/teammates/graph.py +104 -0
  532. novacode_cli/teammates/registry.py +175 -0
  533. novacode_cli/teammates/service.py +254 -0
  534. novacode_cli/teammates/worker.py +19 -0
  535. novacode_cli/teams/__init__.py +1 -0
  536. novacode_cli/teams/adapters/__init__.py +1 -0
  537. novacode_cli/teams/adapters/container.py +316 -0
  538. novacode_cli/teams/adapters/inprocess.py +206 -0
  539. novacode_cli/teams/adapters/process.py +364 -0
  540. novacode_cli/teams/bootstrap.py +246 -0
  541. novacode_cli/teams/container_git.py +67 -0
  542. novacode_cli/teams/context.py +34 -0
  543. novacode_cli/teams/coordinator.py +815 -0
  544. novacode_cli/teams/gateway.py +200 -0
  545. novacode_cli/teams/host.py +64 -0
  546. novacode_cli/teams/legacy.py +52 -0
  547. novacode_cli/teams/middleware.py +176 -0
  548. novacode_cli/teams/models.py +104 -0
  549. novacode_cli/teams/output.py +47 -0
  550. novacode_cli/teams/protocol.py +50 -0
  551. novacode_cli/teams/recovery.py +80 -0
  552. novacode_cli/teams/scheduler.py +44 -0
  553. novacode_cli/teams/store.py +631 -0
  554. novacode_cli/teams/worker.py +251 -0
  555. novacode_cli/teams/workspace.py +100 -0
  556. novacode_cli/token_utils.py +256 -0
  557. novacode_cli/tools/__init__.py +104 -0
  558. novacode_cli/tools/_shared.py +97 -0
  559. novacode_cli/tools/_types.py +24 -0
  560. novacode_cli/tools/artifact_tools.py +137 -0
  561. novacode_cli/tools/code_search_tools.py +330 -0
  562. novacode_cli/tools/computer_tools.py +77 -0
  563. novacode_cli/tools/cron_tools.py +65 -0
  564. novacode_cli/tools/daemon_tool.py +290 -0
  565. novacode_cli/tools/desktop_tools.py +161 -0
  566. novacode_cli/tools/fetch_tools.py +530 -0
  567. novacode_cli/tools/http_tools.py +279 -0
  568. novacode_cli/tools/inspection_tools.py +107 -0
  569. novacode_cli/tools/job_tools.py +143 -0
  570. novacode_cli/tools/knowledge_tools.py +427 -0
  571. novacode_cli/tools/memory_tools.py +461 -0
  572. novacode_cli/tools/memory_wiki_tools.py +40 -0
  573. novacode_cli/tools/notebook_tools.py +238 -0
  574. novacode_cli/tools/oracle_tool.py +83 -0
  575. novacode_cli/tools/plan_archive_tools.py +80 -0
  576. novacode_cli/tools/plan_mode_tools.py +315 -0
  577. novacode_cli/tools/python_kernel_loop.py +165 -0
  578. novacode_cli/tools/python_kernel_tool.py +222 -0
  579. novacode_cli/tools/reflection_tools.py +34 -0
  580. novacode_cli/tools/scraper_tools.py +377 -0
  581. novacode_cli/tools/skill_tools.py +275 -0
  582. novacode_cli/tools/speak_tool.py +51 -0
  583. novacode_cli/tools/store_memory_tools.py +227 -0
  584. novacode_cli/tools/ui_tools.py +62 -0
  585. novacode_cli/tools/waiter_tools.py +408 -0
  586. novacode_cli/tools/web_tools.py +349 -0
  587. novacode_cli/tools/wiki_tools.py +152 -0
  588. novacode_cli/tools/worktree_tools.py +219 -0
  589. novacode_cli/tracking/__init__.py +10 -0
  590. novacode_cli/tracking/file_tracker.py +826 -0
  591. novacode_cli/tracking/local_metrics.py +170 -0
  592. novacode_cli/tracking/loop_guard.py +507 -0
  593. novacode_cli/tracking/request_metrics.py +60 -0
  594. novacode_cli/tracking/tracing.py +692 -0
  595. novacode_cli/tracking/usage_tree.py +677 -0
  596. novacode_cli/tracking/workspace_anchoring.py +231 -0
  597. novacode_cli/tui/__init__.py +11 -0
  598. novacode_cli/tui/animation_rate.py +9 -0
  599. novacode_cli/tui/animations.py +332 -0
  600. novacode_cli/tui/app.py +19315 -0
  601. novacode_cli/tui/auth_display.py +87 -0
  602. novacode_cli/tui/auth_screens.py +826 -0
  603. novacode_cli/tui/avatar.py +294 -0
  604. novacode_cli/tui/avatar_frames.py +21 -0
  605. novacode_cli/tui/background_tasks.py +35 -0
  606. novacode_cli/tui/browser_settings_screen.py +70 -0
  607. novacode_cli/tui/caveman_screen.py +185 -0
  608. novacode_cli/tui/council_settings_screen.py +182 -0
  609. novacode_cli/tui/council_view.py +535 -0
  610. novacode_cli/tui/decision_settings.py +268 -0
  611. novacode_cli/tui/diff_preview.py +199 -0
  612. novacode_cli/tui/gc_tuning.py +63 -0
  613. novacode_cli/tui/handoff_screen.py +206 -0
  614. novacode_cli/tui/harness.py +244 -0
  615. novacode_cli/tui/history_tree_screen.py +179 -0
  616. novacode_cli/tui/home.py +328 -0
  617. novacode_cli/tui/home_scene.py +94 -0
  618. novacode_cli/tui/icons.py +81 -0
  619. novacode_cli/tui/interface_styles.tcss +327 -0
  620. novacode_cli/tui/matrix3.py +108 -0
  621. novacode_cli/tui/mcp_tools_screen.py +131 -0
  622. novacode_cli/tui/motion.py +178 -0
  623. novacode_cli/tui/output_body.py +136 -0
  624. novacode_cli/tui/output_buffer.py +59 -0
  625. novacode_cli/tui/palette.py +334 -0
  626. novacode_cli/tui/pickers.py +237 -0
  627. novacode_cli/tui/prime_panel.py +506 -0
  628. novacode_cli/tui/provider_signin.py +213 -0
  629. novacode_cli/tui/pty_shell.py +159 -0
  630. novacode_cli/tui/reference_screens.py +444 -0
  631. novacode_cli/tui/resume_picker.py +346 -0
  632. novacode_cli/tui/rooms.py +597 -0
  633. novacode_cli/tui/screens.py +7088 -0
  634. novacode_cli/tui/session_import.py +333 -0
  635. novacode_cli/tui/session_pane.py +242 -0
  636. novacode_cli/tui/session_tab.py +42 -0
  637. novacode_cli/tui/session_transcript.py +118 -0
  638. novacode_cli/tui/shell_output.py +7 -0
  639. novacode_cli/tui/stall_watch.py +174 -0
  640. novacode_cli/tui/statusline.py +40 -0
  641. novacode_cli/tui/statusline_screen.py +91 -0
  642. novacode_cli/tui/teammate_updates.py +162 -0
  643. novacode_cli/tui/teammates.py +548 -0
  644. novacode_cli/tui/terminal.py +289 -0
  645. novacode_cli/tui/terminal_view.py +235 -0
  646. novacode_cli/tui/themes.py +118 -0
  647. novacode_cli/tui/update_notice.py +84 -0
  648. novacode_cli/tui/voice_downloads.py +113 -0
  649. novacode_cli/tui/widgets.py +2038 -0
  650. novacode_cli/tui/windows_input.py +38 -0
  651. novacode_cli/tui/workspace_approval.py +165 -0
  652. novacode_cli/ui/council_web.py +113 -0
  653. novacode_cli/ui/council_workspace.py +72 -0
  654. novacode_cli/ui/cowork_web.py +41 -0
  655. novacode_cli/ui/diff_highlight.py +160 -0
  656. novacode_cli/ui/execution.py +1396 -0
  657. novacode_cli/ui/exit_summary.py +65 -0
  658. novacode_cli/ui/hitl_approval.py +196 -0
  659. novacode_cli/ui/input_preparation.py +2 -0
  660. novacode_cli/ui/interrupt_handlers.py +215 -0
  661. novacode_cli/ui/question_prompt.py +151 -0
  662. novacode_cli/ui/status_phrases.py +157 -0
  663. novacode_cli/ui/stream_preview.py +66 -0
  664. novacode_cli/ui/streaming.py +2 -0
  665. novacode_cli/ui/subagent_tracking.py +2 -0
  666. novacode_cli/ui/ui_elements.py +1585 -0
  667. novacode_cli/ui/web_workspace.py +244 -0
  668. novacode_cli/ui_events.py +299 -0
  669. novacode_cli/update_ui.py +243 -0
  670. novacode_cli/updates.py +489 -0
  671. novacode_cli/utils/backend_patches.py +399 -0
  672. novacode_cli/utils/backend_paths.py +467 -0
  673. novacode_cli/utils/lazy_providers.py +51 -0
  674. novacode_cli/utils/model_info.py +223 -0
  675. novacode_cli/utils/pdf_extraction.py +183 -0
  676. novacode_cli/utils/startup.py +29 -0
  677. novacode_cli/utils/tasks.py +42 -0
  678. novacode_cli/utils/windows.py +9 -0
  679. novacode_cli/vixie/hook_adapter.py +127 -0
  680. novacode_cli/vixie/server.py +375 -0
  681. novacode_cli/wiki/__init__.py +15 -0
  682. novacode_cli/wiki/ask.py +56 -0
  683. novacode_cli/wiki/file.py +71 -0
  684. novacode_cli/wiki/ingest.py +184 -0
  685. novacode_cli/wiki/manager.py +261 -0
  686. novacode_cli-1.2.0.dist-info/METADATA +271 -0
  687. novacode_cli-1.2.0.dist-info/RECORD +691 -0
  688. novacode_cli-1.2.0.dist-info/WHEEL +5 -0
  689. novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
  690. novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
  691. 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] + "…"