code-puppy-core-plugins 0.0.2__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 (313) hide show
  1. code_puppy_core_plugins/__init__.py +5 -0
  2. code_puppy_core_plugins/acp/README.md +266 -0
  3. code_puppy_core_plugins/acp/__init__.py +8 -0
  4. code_puppy_core_plugins/acp/agent.py +494 -0
  5. code_puppy_core_plugins/acp/bridge.py +309 -0
  6. code_puppy_core_plugins/acp/capabilities.py +73 -0
  7. code_puppy_core_plugins/acp/commands.py +116 -0
  8. code_puppy_core_plugins/acp/content.py +115 -0
  9. code_puppy_core_plugins/acp/io_delegation.py +231 -0
  10. code_puppy_core_plugins/acp/mcp_config.py +66 -0
  11. code_puppy_core_plugins/acp/permissions.py +159 -0
  12. code_puppy_core_plugins/acp/persistence.py +197 -0
  13. code_puppy_core_plugins/acp/register_callbacks.py +110 -0
  14. code_puppy_core_plugins/acp/replay.py +114 -0
  15. code_puppy_core_plugins/acp/session.py +329 -0
  16. code_puppy_core_plugins/acp/session_config.py +179 -0
  17. code_puppy_core_plugins/acp/state.py +135 -0
  18. code_puppy_core_plugins/agent_skills/__init__.py +22 -0
  19. code_puppy_core_plugins/agent_skills/config.py +208 -0
  20. code_puppy_core_plugins/agent_skills/discovery.py +337 -0
  21. code_puppy_core_plugins/agent_skills/downloader.py +392 -0
  22. code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
  23. code_puppy_core_plugins/agent_skills/installer.py +22 -0
  24. code_puppy_core_plugins/agent_skills/metadata.py +219 -0
  25. code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
  26. code_puppy_core_plugins/agent_skills/provider.py +52 -0
  27. code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
  28. code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
  29. code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
  30. code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
  31. code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
  32. code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
  33. code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
  34. code_puppy_core_plugins/aws_bedrock/config.py +101 -0
  35. code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
  36. code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
  37. code_puppy_core_plugins/azure_foundry/README.md +238 -0
  38. code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
  39. code_puppy_core_plugins/azure_foundry/config.py +126 -0
  40. code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
  41. code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
  42. code_puppy_core_plugins/azure_foundry/token.py +182 -0
  43. code_puppy_core_plugins/azure_foundry/utils.py +375 -0
  44. code_puppy_core_plugins/btw/__init__.py +1 -0
  45. code_puppy_core_plugins/btw/inline_view.py +135 -0
  46. code_puppy_core_plugins/btw/register_callbacks.py +106 -0
  47. code_puppy_core_plugins/btw/side_query.py +91 -0
  48. code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
  49. code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
  50. code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
  51. code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
  52. code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
  53. code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
  54. code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
  55. code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
  56. code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
  57. code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
  58. code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
  59. code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
  60. code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
  61. code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
  62. code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
  63. code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
  64. code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
  65. code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
  66. code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
  67. code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
  68. code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
  69. code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
  70. code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
  71. code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
  72. code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
  73. code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
  74. code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
  75. code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
  76. code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
  77. code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
  78. code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
  79. code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
  80. code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
  81. code_puppy_core_plugins/computer_use/README.md +109 -0
  82. code_puppy_core_plugins/computer_use/__init__.py +1 -0
  83. code_puppy_core_plugins/computer_use/accessibility.py +163 -0
  84. code_puppy_core_plugins/computer_use/activation.py +75 -0
  85. code_puppy_core_plugins/computer_use/backend.py +584 -0
  86. code_puppy_core_plugins/computer_use/backend_types.py +5 -0
  87. code_puppy_core_plugins/computer_use/capture.py +110 -0
  88. code_puppy_core_plugins/computer_use/commands.py +62 -0
  89. code_puppy_core_plugins/computer_use/geometry.py +81 -0
  90. code_puppy_core_plugins/computer_use/inline_image.py +56 -0
  91. code_puppy_core_plugins/computer_use/keycodes.py +47 -0
  92. code_puppy_core_plugins/computer_use/policy.py +144 -0
  93. code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
  94. code_puppy_core_plugins/computer_use/safety.py +18 -0
  95. code_puppy_core_plugins/computer_use/settle.py +64 -0
  96. code_puppy_core_plugins/computer_use/state.py +86 -0
  97. code_puppy_core_plugins/computer_use/tools.py +469 -0
  98. code_puppy_core_plugins/context_indicator/__init__.py +14 -0
  99. code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
  100. code_puppy_core_plugins/context_indicator/usage.py +40 -0
  101. code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
  102. code_puppy_core_plugins/copilot_auth/config.py +93 -0
  103. code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
  104. code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
  105. code_puppy_core_plugins/copilot_auth/utils.py +587 -0
  106. code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
  107. code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
  108. code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
  109. code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
  110. code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
  111. code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
  112. code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
  113. code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
  114. code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
  115. code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
  116. code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
  117. code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
  118. code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
  119. code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
  120. code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
  121. code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
  122. code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
  123. code_puppy_core_plugins/emoji_filter/config.py +26 -0
  124. code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
  125. code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
  126. code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
  127. code_puppy_core_plugins/example_custom_command/README.md +328 -0
  128. code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
  129. code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
  130. code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
  131. code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
  132. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
  133. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
  134. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
  135. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
  136. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
  137. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
  138. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
  139. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
  140. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
  141. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
  142. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
  143. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
  144. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
  145. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
  146. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
  147. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
  148. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
  149. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
  150. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
  151. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
  152. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
  153. code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
  154. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
  155. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
  156. code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
  157. code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
  158. code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
  159. code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
  160. code_puppy_core_plugins/force_push_guard/detector.py +96 -0
  161. code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
  162. code_puppy_core_plugins/fork/__init__.py +7 -0
  163. code_puppy_core_plugins/fork/register_callbacks.py +445 -0
  164. code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
  165. code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
  166. code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
  167. code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
  168. code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
  169. code_puppy_core_plugins/grok_oauth/config.py +49 -0
  170. code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
  171. code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
  172. code_puppy_core_plugins/grok_oauth/utils.py +195 -0
  173. code_puppy_core_plugins/herdr/README.md +168 -0
  174. code_puppy_core_plugins/herdr/__init__.py +9 -0
  175. code_puppy_core_plugins/herdr/client.py +339 -0
  176. code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
  177. code_puppy_core_plugins/herdr/reporter.py +212 -0
  178. code_puppy_core_plugins/herdr/smoke.py +143 -0
  179. code_puppy_core_plugins/herdr/sources.py +133 -0
  180. code_puppy_core_plugins/hook_creator/__init__.py +1 -0
  181. code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
  182. code_puppy_core_plugins/hook_manager/__init__.py +1 -0
  183. code_puppy_core_plugins/hook_manager/config.py +290 -0
  184. code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
  185. code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
  186. code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
  187. code_puppy_core_plugins/meta_oauth/config.py +55 -0
  188. code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
  189. code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
  190. code_puppy_core_plugins/meta_oauth/utils.py +252 -0
  191. code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
  192. code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
  193. code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
  194. code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
  195. code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
  196. code_puppy_core_plugins/no_tools/__init__.py +0 -0
  197. code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
  198. code_puppy_core_plugins/oauth_pasteback.py +110 -0
  199. code_puppy_core_plugins/oauth_puppy_html.py +194 -0
  200. code_puppy_core_plugins/obsidian_agent/README.md +52 -0
  201. code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
  202. code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
  203. code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
  204. code_puppy_core_plugins/ollama/__init__.py +1 -0
  205. code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
  206. code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
  207. code_puppy_core_plugins/ollama_setup/completer.py +38 -0
  208. code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
  209. code_puppy_core_plugins/plugin_list/__init__.py +0 -0
  210. code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
  211. code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
  212. code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
  213. code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
  214. code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
  215. code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
  216. code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
  217. code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
  218. code_puppy_core_plugins/pop_command/__init__.py +1 -0
  219. code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
  220. code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
  221. code_puppy_core_plugins/prompt_newline/config.py +21 -0
  222. code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
  223. code_puppy_core_plugins/prune/__init__.py +1 -0
  224. code_puppy_core_plugins/prune/prune_menu.py +308 -0
  225. code_puppy_core_plugins/prune/prune_model.py +517 -0
  226. code_puppy_core_plugins/prune/prune_render.py +354 -0
  227. code_puppy_core_plugins/prune/register_callbacks.py +388 -0
  228. code_puppy_core_plugins/puppy_kennel/README.md +203 -0
  229. code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
  230. code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
  231. code_puppy_core_plugins/puppy_kennel/config.py +36 -0
  232. code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
  233. code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
  234. code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
  235. code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
  236. code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
  237. code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
  238. code_puppy_core_plugins/puppy_kennel/state.py +68 -0
  239. code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
  240. code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
  241. code_puppy_core_plugins/puppy_spinner/README.md +93 -0
  242. code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
  243. code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
  244. code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
  245. code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
  246. code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
  247. code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
  248. code_puppy_core_plugins/quick_resume/__init__.py +0 -0
  249. code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
  250. code_puppy_core_plugins/review_pr/__init__.py +1 -0
  251. code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
  252. code_puppy_core_plugins/shell_safety/__init__.py +6 -0
  253. code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
  254. code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
  255. code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
  256. code_puppy_core_plugins/stack_dump/__init__.py +1 -0
  257. code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
  258. code_puppy_core_plugins/statusline/__init__.py +14 -0
  259. code_puppy_core_plugins/statusline/config.py +68 -0
  260. code_puppy_core_plugins/statusline/payload.py +138 -0
  261. code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
  262. code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
  263. code_puppy_core_plugins/statusline/runner.py +122 -0
  264. code_puppy_core_plugins/statusline/statusline_command.py +197 -0
  265. code_puppy_core_plugins/steer_queue/__init__.py +10 -0
  266. code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
  267. code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
  268. code_puppy_core_plugins/subagent_panel/README.md +102 -0
  269. code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
  270. code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
  271. code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
  272. code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
  273. code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
  274. code_puppy_core_plugins/subagent_panel/state.py +231 -0
  275. code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
  276. code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
  277. code_puppy_core_plugins/theme/README.md +86 -0
  278. code_puppy_core_plugins/theme/__init__.py +14 -0
  279. code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
  280. code_puppy_core_plugins/theme/content_styles.py +146 -0
  281. code_puppy_core_plugins/theme/osc_palette.py +178 -0
  282. code_puppy_core_plugins/theme/picker.py +358 -0
  283. code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
  284. code_puppy_core_plugins/theme/register_callbacks.py +348 -0
  285. code_puppy_core_plugins/theme/rich_themes.py +202 -0
  286. code_puppy_core_plugins/theme/themes.py +720 -0
  287. code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
  288. code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
  289. code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
  290. code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
  291. code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
  292. code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
  293. code_puppy_core_plugins/universal_constructor/models.py +138 -0
  294. code_puppy_core_plugins/universal_constructor/provider.py +62 -0
  295. code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
  296. code_puppy_core_plugins/universal_constructor/registry.py +302 -0
  297. code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
  298. code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
  299. code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
  300. code_puppy_core_plugins/wiggum/__init__.py +1 -0
  301. code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
  302. code_puppy_core_plugins/wiggum/judge.py +299 -0
  303. code_puppy_core_plugins/wiggum/judge_config.py +249 -0
  304. code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
  305. code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
  306. code_puppy_core_plugins/wiggum/state.py +89 -0
  307. code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
  308. code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
  309. code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
  310. code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
  311. code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
  312. code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
  313. code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,814 @@
1
+ """Interactive terminal UI for configuring goal-mode LLM judges.
2
+
3
+ A split-panel list (left = judges, right = preview), with an in-TUI
4
+ form for adding/editing — no $EDITOR popout. Everything happens in
5
+ prompt_toolkit, so the UX stays inside the terminal session.
6
+
7
+ List view keys:
8
+ N add new judge (opens form)
9
+ Enter / E edit selected judge (opens form)
10
+ T toggle enabled
11
+ D delete selected
12
+ Esc / Ctrl+C close menu
13
+
14
+ Form view keys:
15
+ Tab / Shift+Tab cycle between Name ↔ Model ↔ Prompt
16
+ ↑ / ↓ (when Model is focused) select model
17
+ ←→ / PgUp PgDn (when Model is focused) page through models
18
+ Home / End (when Model is focused) jump to first / last model
19
+ Ctrl+S save
20
+ Esc / Ctrl+C cancel
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import sys
27
+ import unicodedata
28
+ from typing import Optional
29
+
30
+ from prompt_toolkit.application import Application
31
+ from prompt_toolkit.key_binding import KeyBindings
32
+ from prompt_toolkit.layout import Dimension, HSplit, Layout, VSplit, Window
33
+ from prompt_toolkit.layout.controls import FormattedTextControl
34
+ from prompt_toolkit.widgets import Frame, TextArea
35
+
36
+ from code_puppy.command_line.model_picker_completion import load_model_names
37
+ from code_puppy.command_line.pagination import (
38
+ ensure_visible_page,
39
+ get_page_bounds,
40
+ get_page_for_index,
41
+ get_total_pages,
42
+ )
43
+ from code_puppy.messaging import emit_info, emit_success, emit_warning
44
+ from .judge_config import (
45
+ DEFAULT_JUDGE_PROMPT,
46
+ JudgeConfig,
47
+ add_judge,
48
+ delete_judge,
49
+ load_judges,
50
+ toggle_judge,
51
+ update_judge,
52
+ validate_name,
53
+ )
54
+ from code_puppy.tools.command_runner import set_awaiting_user_input
55
+ from code_puppy.callbacks import on_prompt_toolkit_style
56
+
57
+ PAGE_SIZE = 10
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Display helpers
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ def _sanitize(text: str) -> str:
66
+ """Strip characters that mess with prompt_toolkit width calculations."""
67
+ safe = (
68
+ "Lu",
69
+ "Ll",
70
+ "Lt",
71
+ "Lm",
72
+ "Lo",
73
+ "Nd",
74
+ "Nl",
75
+ "No",
76
+ "Pc",
77
+ "Pd",
78
+ "Ps",
79
+ "Pe",
80
+ "Pi",
81
+ "Pf",
82
+ "Po",
83
+ "Zs",
84
+ "Sm",
85
+ "Sc",
86
+ "Sk",
87
+ )
88
+ cleaned = "".join(c for c in text if unicodedata.category(c) in safe)
89
+ return " ".join(cleaned.split())
90
+
91
+
92
+ def _wrap(text: str, width: int) -> list[str]:
93
+ """Crude word-wrap for the preview panel."""
94
+ out: list[str] = []
95
+ for line in text.split("\n"):
96
+ if not line.strip():
97
+ out.append("")
98
+ continue
99
+ words = line.split()
100
+ current = ""
101
+ for word in words:
102
+ if not current:
103
+ current = word
104
+ elif len(current) + 1 + len(word) > width:
105
+ out.append(current)
106
+ current = word
107
+ else:
108
+ current += " " + word
109
+ if current:
110
+ out.append(current)
111
+ return out
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Model list (inline paginated picker rendered as a tabbable form section)
116
+ # ---------------------------------------------------------------------------
117
+
118
+ MODEL_PAGE_SIZE = 8 # rows of models visible at once in the form section
119
+
120
+
121
+ def _load_available_models() -> list[str]:
122
+ """Return the list of model names, or [] if loading fails."""
123
+ try:
124
+ return load_model_names() or []
125
+ except Exception as exc:
126
+ emit_warning(f"Failed to load models: {exc}")
127
+ return []
128
+
129
+
130
+ def _render_model_list(
131
+ models: list[str],
132
+ selected_idx: int,
133
+ page: int,
134
+ *,
135
+ focused: bool,
136
+ ) -> list:
137
+ """Render the inline paginated model list with a selection marker."""
138
+ lines: list = []
139
+
140
+ if not models:
141
+ lines.append(("class:tui.warning", " No models available."))
142
+ lines.append(("", "\n"))
143
+ lines.append(
144
+ (
145
+ "class:tui.muted",
146
+ " Configure models first — see /model in the main CLI.",
147
+ )
148
+ )
149
+ return lines
150
+
151
+ total_pages = get_total_pages(len(models), MODEL_PAGE_SIZE)
152
+ start, end = get_page_bounds(page, len(models), MODEL_PAGE_SIZE)
153
+
154
+ # Header: (Page x/y, focused indicator)
155
+ if focused:
156
+ lines.append(("class:tui.selected", "▼ "))
157
+ else:
158
+ lines.append(("class:tui.muted", " "))
159
+ lines.append(
160
+ (
161
+ "class:tui.muted",
162
+ f"Page {page + 1}/{max(total_pages, 1)} "
163
+ f"(↑↓ to move, ←→ / PgUp PgDn to page)\n",
164
+ )
165
+ )
166
+
167
+ for i in range(start, end):
168
+ is_sel = i == selected_idx
169
+ name = _sanitize(models[i])
170
+ if is_sel and focused:
171
+ lines.append(("class:tui.selected", " ▶ "))
172
+ lines.append(("class:tui.selected", name))
173
+ elif is_sel:
174
+ lines.append(("class:tui.warning", " · "))
175
+ lines.append(("class:tui.warning", name))
176
+ else:
177
+ lines.append(("", " "))
178
+ lines.append(("", name))
179
+ lines.append(("", "\n"))
180
+
181
+ return lines
182
+
183
+
184
+ # ---------------------------------------------------------------------------
185
+ # In-TUI form for add/edit
186
+ # ---------------------------------------------------------------------------
187
+
188
+
189
+ class _FormResult:
190
+ """Mutable struct so closures in key bindings can mutate."""
191
+
192
+ def __init__(self) -> None:
193
+ self.saved: bool = False
194
+ self.cancelled: bool = False
195
+ self.name: str = ""
196
+ self.model: str = ""
197
+ self.prompt: str = ""
198
+
199
+
200
+ async def _run_judge_form(
201
+ *,
202
+ title: str,
203
+ initial_name: str = "",
204
+ initial_model: str = "",
205
+ initial_prompt: str = DEFAULT_JUDGE_PROMPT,
206
+ ) -> _FormResult:
207
+ """Render the add/edit form with three tabbable sections:
208
+
209
+ 1. Name — single-line TextArea
210
+ 2. Model — inline paginated list (focusable Window)
211
+ 3. Prompt — multiline TextArea
212
+
213
+ Tab / Shift+Tab cycles between sections. When Model is focused, the
214
+ arrow keys move the selection; ←→ / PgUp PgDn page-jump. Whatever's
215
+ highlighted IS the selected model — no separate "confirm" gesture.
216
+ """
217
+ from prompt_toolkit.filters import Condition
218
+
219
+ result = _FormResult()
220
+ status_line = [""]
221
+
222
+ # ---- Model list state ----
223
+ models = _load_available_models()
224
+ # Find the index of the initial model, or default to 0.
225
+ try:
226
+ model_idx = [models.index(initial_model)] if initial_model in models else [0]
227
+ except ValueError:
228
+ model_idx = [0]
229
+ model_page = [get_page_for_index(model_idx[0], MODEL_PAGE_SIZE) if models else 0]
230
+
231
+ def current_model() -> str:
232
+ if not models:
233
+ return ""
234
+ return models[max(0, min(model_idx[0], len(models) - 1))]
235
+
236
+ # ---- Widgets ----
237
+ name_area = TextArea(
238
+ text=initial_name,
239
+ multiline=False,
240
+ wrap_lines=False,
241
+ focusable=True,
242
+ height=1,
243
+ )
244
+ prompt_area = TextArea(
245
+ text=initial_prompt,
246
+ multiline=True,
247
+ wrap_lines=True,
248
+ focusable=True,
249
+ scrollbar=True,
250
+ height=Dimension(min=6, weight=55),
251
+ )
252
+
253
+ model_control = FormattedTextControl(
254
+ text="",
255
+ focusable=True,
256
+ show_cursor=False,
257
+ )
258
+ model_window = Window(
259
+ content=model_control,
260
+ wrap_lines=False,
261
+ # +2 rows for the header line and padding.
262
+ height=Dimension(min=MODEL_PAGE_SIZE + 2, max=MODEL_PAGE_SIZE + 2),
263
+ )
264
+
265
+ status_control = FormattedTextControl(text="")
266
+ help_control = FormattedTextControl(text="")
267
+ status_window = Window(content=status_control, height=1)
268
+ help_window = Window(content=help_control, height=1)
269
+
270
+ # Layout: stacked frames, Name fixed-height, Model fixed-height,
271
+ # Prompt takes the rest of the column.
272
+ model_frame = Frame(model_window, title="Model")
273
+ root = HSplit(
274
+ [
275
+ Frame(name_area, title="Name", height=3),
276
+ model_frame,
277
+ Frame(prompt_area, title="Prompt (multiline)"),
278
+ status_window,
279
+ help_window,
280
+ ]
281
+ )
282
+
283
+ # ---- Renderers ----
284
+ def is_model_focused() -> bool:
285
+ try:
286
+ return app.layout.current_window is model_window
287
+ except Exception:
288
+ return False
289
+
290
+ def refresh() -> None:
291
+ model_control.text = _render_model_list(
292
+ models,
293
+ model_idx[0],
294
+ model_page[0],
295
+ focused=is_model_focused(),
296
+ )
297
+ if status_line[0]:
298
+ status_control.text = [("class:tui.error", status_line[0])]
299
+ else:
300
+ # Hint at the bottom of the form: show current model selection.
301
+ current = current_model() or "(no models available)"
302
+ status_control.text = [
303
+ ("class:tui.muted", "Selected model: "),
304
+ ("class:tui.warning", _sanitize(current)),
305
+ ]
306
+ help_control.text = [
307
+ ("class:tui.help-key", " Tab "),
308
+ ("", "next field "),
309
+ ("class:tui.help-key", " ↑↓ "),
310
+ ("", "select model "),
311
+ ("class:tui.help-key", " Ctrl+S "),
312
+ ("", "save "),
313
+ ("class:tui.error", " Esc/Ctrl+C "),
314
+ ("", "cancel"),
315
+ ]
316
+
317
+ # ---- Keybindings ----
318
+ kb = KeyBindings()
319
+
320
+ # Cycle order: name → model → prompt → name ...
321
+ _focus_cycle = [name_area, model_window, prompt_area]
322
+
323
+ def _focus_index() -> int:
324
+ try:
325
+ cur = app.layout.current_window
326
+ except Exception:
327
+ return 0
328
+ for i, item in enumerate(_focus_cycle):
329
+ target = item.window if hasattr(item, "window") else item
330
+ if cur is target:
331
+ return i
332
+ return 0
333
+
334
+ def _focus(item) -> None:
335
+ # TextArea.focus works on the TextArea; Window is focused directly.
336
+ app.layout.focus(item)
337
+
338
+ @kb.add("tab")
339
+ def _(event):
340
+ nxt = (_focus_index() + 1) % len(_focus_cycle)
341
+ _focus(_focus_cycle[nxt])
342
+ refresh()
343
+
344
+ @kb.add("s-tab")
345
+ def _(event):
346
+ prev = (_focus_index() - 1) % len(_focus_cycle)
347
+ _focus(_focus_cycle[prev])
348
+ refresh()
349
+
350
+ @kb.add("escape")
351
+ @kb.add("c-c")
352
+ def _(event):
353
+ result.cancelled = True
354
+ event.app.exit()
355
+
356
+ @kb.add("c-s")
357
+ def _(event):
358
+ name = name_area.text.strip()
359
+ prompt_text = prompt_area.text
360
+ err = validate_name(name)
361
+ if err:
362
+ status_line[0] = err
363
+ refresh()
364
+ return
365
+ chosen_model = current_model()
366
+ if not chosen_model:
367
+ status_line[0] = "No models available — cannot save."
368
+ refresh()
369
+ return
370
+ if not prompt_text.strip():
371
+ status_line[0] = "Prompt cannot be empty."
372
+ refresh()
373
+ return
374
+ result.saved = True
375
+ result.name = name
376
+ result.model = chosen_model
377
+ result.prompt = prompt_text
378
+ event.app.exit()
379
+
380
+ # ---- Model-list navigation (only fires when the model window is focused) ----
381
+ model_focused = Condition(is_model_focused)
382
+
383
+ @kb.add("up", filter=model_focused)
384
+ def _(event):
385
+ if not models:
386
+ return
387
+ if model_idx[0] > 0:
388
+ model_idx[0] -= 1
389
+ model_page[0] = ensure_visible_page(
390
+ model_idx[0], model_page[0], len(models), MODEL_PAGE_SIZE
391
+ )
392
+ status_line[0] = ""
393
+ refresh()
394
+
395
+ @kb.add("down", filter=model_focused)
396
+ def _(event):
397
+ if not models:
398
+ return
399
+ if model_idx[0] < len(models) - 1:
400
+ model_idx[0] += 1
401
+ model_page[0] = ensure_visible_page(
402
+ model_idx[0], model_page[0], len(models), MODEL_PAGE_SIZE
403
+ )
404
+ status_line[0] = ""
405
+ refresh()
406
+
407
+ def _page_jump(delta: int) -> None:
408
+ if not models:
409
+ return
410
+ total = get_total_pages(len(models), MODEL_PAGE_SIZE)
411
+ new_page = max(0, min(model_page[0] + delta, total - 1))
412
+ if new_page == model_page[0]:
413
+ return
414
+ model_page[0] = new_page
415
+ # Snap selection to the first item on the new page.
416
+ model_idx[0] = new_page * MODEL_PAGE_SIZE
417
+ status_line[0] = ""
418
+ refresh()
419
+
420
+ @kb.add("left", filter=model_focused)
421
+ @kb.add("pageup", filter=model_focused)
422
+ def _(event):
423
+ _page_jump(-1)
424
+
425
+ @kb.add("right", filter=model_focused)
426
+ @kb.add("pagedown", filter=model_focused)
427
+ def _(event):
428
+ _page_jump(1)
429
+
430
+ @kb.add("home", filter=model_focused)
431
+ def _(event):
432
+ if not models:
433
+ return
434
+ model_idx[0] = 0
435
+ model_page[0] = 0
436
+ status_line[0] = ""
437
+ refresh()
438
+
439
+ @kb.add("end", filter=model_focused)
440
+ def _(event):
441
+ if not models:
442
+ return
443
+ model_idx[0] = len(models) - 1
444
+ model_page[0] = get_page_for_index(model_idx[0], MODEL_PAGE_SIZE)
445
+ status_line[0] = ""
446
+ refresh()
447
+
448
+ # ---- App ----
449
+ layout = Layout(root)
450
+ app = Application(
451
+ layout=layout,
452
+ key_bindings=kb,
453
+ full_screen=False,
454
+ mouse_support=False,
455
+ style=on_prompt_toolkit_style(),
456
+ )
457
+ layout.focus(name_area)
458
+
459
+ sys.stdout.write("\033[2J\033[H")
460
+ sys.stdout.flush()
461
+ refresh()
462
+ await app.run_async()
463
+
464
+ return result
465
+
466
+
467
+ # ---------------------------------------------------------------------------
468
+ # Panel rendering for the list view
469
+ # ---------------------------------------------------------------------------
470
+
471
+
472
+ def _render_menu(
473
+ judges: list[JudgeConfig],
474
+ page: int,
475
+ selected_idx: int,
476
+ ) -> list:
477
+ lines = []
478
+ total_pages = get_total_pages(len(judges), PAGE_SIZE)
479
+ start, end = get_page_bounds(page, len(judges), PAGE_SIZE)
480
+
481
+ lines.append(("class:tui.header", "Goal Judges"))
482
+ lines.append(("class:tui.muted", f" (Page {page + 1}/{max(total_pages, 1)})"))
483
+ lines.append(("", "\n\n"))
484
+
485
+ if not judges:
486
+ lines.append(("class:tui.warning", " No judges configured."))
487
+ lines.append(("", "\n"))
488
+ lines.append(("class:tui.muted", " Press "))
489
+ lines.append(("class:tui.help-key", "N"))
490
+ lines.append(("class:tui.muted", " to add one."))
491
+ lines.append(("", "\n\n"))
492
+ else:
493
+ for i in range(start, end):
494
+ judge = judges[i]
495
+ is_selected = i == selected_idx
496
+ marker = "▶ " if is_selected else " "
497
+ row_style = "class:tui.selected" if is_selected else ""
498
+ enabled_glyph = "[on] " if judge.enabled else "[off]"
499
+ enabled_style = "class:tui.success" if judge.enabled else "class:tui.muted"
500
+
501
+ lines.append((row_style or "class:tui.success", marker))
502
+ lines.append((enabled_style, enabled_glyph + " "))
503
+ lines.append((row_style, _sanitize(judge.name)))
504
+ lines.append(("class:tui.muted", " "))
505
+ lines.append(("class:tui.warning", _sanitize(judge.model)))
506
+ lines.append(("", "\n"))
507
+
508
+ lines.append(("", "\n"))
509
+ lines.append(("class:tui.help-key", " ↑↓ "))
510
+ lines.append(("", "Navigate\n"))
511
+ lines.append(("class:tui.help-key", " ←→ "))
512
+ lines.append(("", "Page\n"))
513
+ lines.append(("class:tui.help-key", " N "))
514
+ lines.append(("", "New judge\n"))
515
+ lines.append(("class:tui.help-key", " Enter "))
516
+ lines.append(("", "Edit (or E)\n"))
517
+ lines.append(("class:tui.help-key", " T "))
518
+ lines.append(("", "Toggle enabled\n"))
519
+ lines.append(("class:tui.error", " D "))
520
+ lines.append(("", "Delete\n"))
521
+ lines.append(("class:tui.help-key", " Esc "))
522
+ lines.append(("", "Close (or Ctrl+C)"))
523
+ return lines
524
+
525
+
526
+ def _render_preview(judge: Optional[JudgeConfig]) -> list:
527
+ lines = []
528
+ lines.append(("class:tui.title", " JUDGE DETAILS"))
529
+ lines.append(("", "\n\n"))
530
+
531
+ if judge is None:
532
+ lines.append(("class:tui.warning", " No judge selected."))
533
+ lines.append(("", "\n"))
534
+ return lines
535
+
536
+ lines.append(("class:tui.label", "Name: "))
537
+ lines.append(("", _sanitize(judge.name)))
538
+ lines.append(("", "\n\n"))
539
+
540
+ lines.append(("class:tui.label", "Model: "))
541
+ lines.append(("class:tui.warning", _sanitize(judge.model)))
542
+ lines.append(("", "\n\n"))
543
+
544
+ lines.append(("class:tui.label", "Enabled: "))
545
+ if judge.enabled:
546
+ lines.append(("class:tui.success", "yes"))
547
+ else:
548
+ lines.append(("class:tui.muted", "no"))
549
+ lines.append(("", "\n\n"))
550
+
551
+ lines.append(("class:tui.label", "Prompt:"))
552
+ lines.append(("", "\n"))
553
+ for wrapped in _wrap(judge.prompt or "", width=58):
554
+ lines.append(("class:tui.muted", wrapped or " "))
555
+ lines.append(("", "\n"))
556
+
557
+ return lines
558
+
559
+
560
+ # ---------------------------------------------------------------------------
561
+ # Add / edit handlers (invoked between TUI sessions)
562
+ # ---------------------------------------------------------------------------
563
+
564
+
565
+ async def _add_judge_flow() -> Optional[str]:
566
+ form = await _run_judge_form(title="New Judge")
567
+ if not form.saved:
568
+ emit_info("Cancelled.")
569
+ return None
570
+ try:
571
+ add_judge(
572
+ JudgeConfig(
573
+ name=form.name,
574
+ model=form.model,
575
+ prompt=form.prompt,
576
+ enabled=True,
577
+ )
578
+ )
579
+ except ValueError as exc:
580
+ emit_warning(str(exc))
581
+ return None
582
+ emit_success(f"Added judge {form.name!r} → {form.model}")
583
+ return form.name
584
+
585
+
586
+ async def _edit_judge_flow(current: JudgeConfig) -> Optional[str]:
587
+ form = await _run_judge_form(
588
+ title=f"Edit Judge — {current.name}",
589
+ initial_name=current.name,
590
+ initial_model=current.model,
591
+ initial_prompt=current.prompt,
592
+ )
593
+ if not form.saved:
594
+ emit_info("Cancelled.")
595
+ return current.name
596
+
597
+ try:
598
+ update_judge(
599
+ current.name,
600
+ new_name=form.name if form.name != current.name else None,
601
+ model=form.model if form.model != current.model else None,
602
+ prompt=form.prompt if form.prompt != current.prompt else None,
603
+ )
604
+ except ValueError as exc:
605
+ emit_warning(str(exc))
606
+ return current.name
607
+ emit_success(f"Updated judge {form.name!r}")
608
+ return form.name
609
+
610
+
611
+ # ---------------------------------------------------------------------------
612
+ # Main TUI loop
613
+ # ---------------------------------------------------------------------------
614
+
615
+
616
+ async def interactive_judges_menu() -> None:
617
+ """Open the goal-judges TUI. Returns when the user closes the menu."""
618
+ registry = load_judges()
619
+ judges = list(registry.judges)
620
+
621
+ selected_idx = [0]
622
+ current_page = [0]
623
+ pending_action: list[Optional[str]] = [None]
624
+ pending_target: list[Optional[str]] = [None]
625
+
626
+ def refresh(select_name: Optional[str] = None) -> None:
627
+ nonlocal judges
628
+ registry = load_judges()
629
+ judges = list(registry.judges)
630
+ if not judges:
631
+ selected_idx[0] = 0
632
+ current_page[0] = 0
633
+ return
634
+ if select_name:
635
+ for i, j in enumerate(judges):
636
+ if j.name == select_name:
637
+ selected_idx[0] = i
638
+ break
639
+ else:
640
+ selected_idx[0] = min(selected_idx[0], len(judges) - 1)
641
+ else:
642
+ selected_idx[0] = min(selected_idx[0], len(judges) - 1)
643
+ current_page[0] = get_page_for_index(selected_idx[0], PAGE_SIZE)
644
+
645
+ def current_judge() -> Optional[JudgeConfig]:
646
+ if 0 <= selected_idx[0] < len(judges):
647
+ return judges[selected_idx[0]]
648
+ return None
649
+
650
+ menu_control = FormattedTextControl(text="")
651
+ preview_control = FormattedTextControl(text="")
652
+
653
+ def update_display() -> None:
654
+ menu_control.text = _render_menu(judges, current_page[0], selected_idx[0])
655
+ preview_control.text = _render_preview(current_judge())
656
+
657
+ menu_window = Window(
658
+ content=menu_control, wrap_lines=False, width=Dimension(weight=40)
659
+ )
660
+ preview_window = Window(
661
+ content=preview_control, wrap_lines=True, width=Dimension(weight=60)
662
+ )
663
+ menu_frame = Frame(menu_window, width=Dimension(weight=40), title="Judges")
664
+ preview_frame = Frame(preview_window, width=Dimension(weight=60), title="Preview")
665
+ root = VSplit([menu_frame, preview_frame])
666
+
667
+ kb = KeyBindings()
668
+
669
+ @kb.add("up")
670
+ def _(event):
671
+ if selected_idx[0] > 0:
672
+ selected_idx[0] -= 1
673
+ current_page[0] = ensure_visible_page(
674
+ selected_idx[0], current_page[0], len(judges), PAGE_SIZE
675
+ )
676
+ update_display()
677
+
678
+ @kb.add("down")
679
+ def _(event):
680
+ if selected_idx[0] < len(judges) - 1:
681
+ selected_idx[0] += 1
682
+ current_page[0] = ensure_visible_page(
683
+ selected_idx[0], current_page[0], len(judges), PAGE_SIZE
684
+ )
685
+ update_display()
686
+
687
+ @kb.add("left")
688
+ def _(event):
689
+ if current_page[0] > 0:
690
+ current_page[0] -= 1
691
+ selected_idx[0] = current_page[0] * PAGE_SIZE
692
+ update_display()
693
+
694
+ @kb.add("right")
695
+ def _(event):
696
+ total = get_total_pages(len(judges), PAGE_SIZE)
697
+ if current_page[0] < total - 1:
698
+ current_page[0] += 1
699
+ selected_idx[0] = current_page[0] * PAGE_SIZE
700
+ update_display()
701
+
702
+ @kb.add("n")
703
+ def _(event):
704
+ pending_action[0] = "add"
705
+ event.app.exit()
706
+
707
+ # Enter edits the highlighted judge — it's the obvious "act on this row"
708
+ # gesture in a list view. 'E' is kept as an alias for muscle memory.
709
+ @kb.add("enter")
710
+ @kb.add("e")
711
+ def _(event):
712
+ judge = current_judge()
713
+ if judge:
714
+ pending_action[0] = "edit"
715
+ pending_target[0] = judge.name
716
+ event.app.exit()
717
+
718
+ @kb.add("t")
719
+ def _(event):
720
+ judge = current_judge()
721
+ if judge:
722
+ pending_action[0] = "toggle"
723
+ pending_target[0] = judge.name
724
+ event.app.exit()
725
+
726
+ @kb.add("d")
727
+ def _(event):
728
+ judge = current_judge()
729
+ if judge:
730
+ pending_action[0] = "delete"
731
+ pending_target[0] = judge.name
732
+ event.app.exit()
733
+
734
+ # Esc and Ctrl+C both close (Esc = natural "I'm done"; Ctrl+C = escape
735
+ # hatch). eager=False is fine: no Esc-chord in the list view, so the
736
+ # handler fires as soon as the chord timeout expires (immediate in
737
+ # practice).
738
+ @kb.add("escape")
739
+ @kb.add("c-c")
740
+ def _(event):
741
+ pending_action[0] = "close"
742
+ event.app.exit()
743
+
744
+ layout = Layout(root)
745
+ app = Application(
746
+ layout=layout,
747
+ key_bindings=kb,
748
+ full_screen=False,
749
+ mouse_support=False,
750
+ style=on_prompt_toolkit_style(),
751
+ )
752
+
753
+ set_awaiting_user_input(True)
754
+ sys.stdout.write("\033[?1049h")
755
+ sys.stdout.write("\033[2J\033[H")
756
+ sys.stdout.flush()
757
+ await asyncio.sleep(0.05)
758
+
759
+ try:
760
+ while True:
761
+ pending_action[0] = None
762
+ pending_target[0] = None
763
+ update_display()
764
+ sys.stdout.write("\033[2J\033[H")
765
+ sys.stdout.flush()
766
+
767
+ await app.run_async()
768
+
769
+ action = pending_action[0]
770
+ target = pending_target[0]
771
+
772
+ if action in (None, "close", "cancel"):
773
+ break
774
+
775
+ if action == "add":
776
+ new_name = await _add_judge_flow()
777
+ refresh(select_name=new_name)
778
+ continue
779
+
780
+ if not target:
781
+ continue
782
+
783
+ if action == "edit":
784
+ judge = next((j for j in judges if j.name == target), None)
785
+ if judge:
786
+ new_name = await _edit_judge_flow(judge)
787
+ refresh(select_name=new_name or target)
788
+ continue
789
+
790
+ if action == "toggle":
791
+ new_state = toggle_judge(target)
792
+ if new_state is None:
793
+ emit_warning(f"No judge named {target!r}.")
794
+ else:
795
+ emit_info(
796
+ f"{target!r} is now {'enabled' if new_state else 'disabled'}"
797
+ )
798
+ refresh(select_name=target)
799
+ continue
800
+
801
+ if action == "delete":
802
+ if delete_judge(target):
803
+ emit_success(f"Deleted judge {target!r}")
804
+ else:
805
+ emit_warning(f"No judge named {target!r}.")
806
+ refresh()
807
+ continue
808
+
809
+ finally:
810
+ sys.stdout.write("\033[?1049l")
811
+ sys.stdout.flush()
812
+ set_awaiting_user_input(False)
813
+
814
+ emit_info("✓ Exited judges menu")