code-muse 0.0.1__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 (394) hide show
  1. code_muse/__init__.py +26 -0
  2. code_muse/__main__.py +10 -0
  3. code_muse/agents/__init__.py +31 -0
  4. code_muse/agents/_builder.py +214 -0
  5. code_muse/agents/_compaction.py +506 -0
  6. code_muse/agents/_diagnostics.py +171 -0
  7. code_muse/agents/_history.py +382 -0
  8. code_muse/agents/_key_listeners.py +148 -0
  9. code_muse/agents/_non_streaming_render.py +148 -0
  10. code_muse/agents/_runtime.py +596 -0
  11. code_muse/agents/agent_creator_agent.py +603 -0
  12. code_muse/agents/agent_helios.py +47 -0
  13. code_muse/agents/agent_manager.py +740 -0
  14. code_muse/agents/agent_muse.py +78 -0
  15. code_muse/agents/agent_planning.py +44 -0
  16. code_muse/agents/agent_qa_melpomene.py +207 -0
  17. code_muse/agents/base_agent.py +194 -0
  18. code_muse/agents/event_stream_handler.py +361 -0
  19. code_muse/agents/json_agent.py +201 -0
  20. code_muse/agents/prompt_v3.py +521 -0
  21. code_muse/agents/subagent_stream_handler.py +273 -0
  22. code_muse/callbacks.py +941 -0
  23. code_muse/chatgpt_codex_client.py +333 -0
  24. code_muse/claude_cache_client.py +853 -0
  25. code_muse/cli_runner/__init__.py +319 -0
  26. code_muse/cli_runner/args.py +63 -0
  27. code_muse/cli_runner/loop.py +510 -0
  28. code_muse/cli_runner/resume.py +72 -0
  29. code_muse/cli_runner/runner.py +161 -0
  30. code_muse/command_line/__init__.py +1 -0
  31. code_muse/command_line/add_model_menu.py +1331 -0
  32. code_muse/command_line/agent_menu.py +674 -0
  33. code_muse/command_line/attachments.py +397 -0
  34. code_muse/command_line/autosave_menu.py +709 -0
  35. code_muse/command_line/clipboard.py +528 -0
  36. code_muse/command_line/colors_menu.py +530 -0
  37. code_muse/command_line/command_handler.py +262 -0
  38. code_muse/command_line/command_registry.py +150 -0
  39. code_muse/command_line/config_commands.py +711 -0
  40. code_muse/command_line/core_commands.py +740 -0
  41. code_muse/command_line/diff_menu.py +865 -0
  42. code_muse/command_line/file_path_completion.py +73 -0
  43. code_muse/command_line/load_context_completion.py +57 -0
  44. code_muse/command_line/model_picker_completion.py +512 -0
  45. code_muse/command_line/model_settings_menu.py +983 -0
  46. code_muse/command_line/onboarding_slides.py +162 -0
  47. code_muse/command_line/onboarding_wizard.py +337 -0
  48. code_muse/command_line/pagination.py +41 -0
  49. code_muse/command_line/pin_command_completion.py +329 -0
  50. code_muse/command_line/prompt_toolkit_completion.py +886 -0
  51. code_muse/command_line/session_commands.py +304 -0
  52. code_muse/command_line/shell_passthrough.py +145 -0
  53. code_muse/command_line/skills_completion.py +158 -0
  54. code_muse/command_line/types.py +18 -0
  55. code_muse/command_line/uc_menu.py +908 -0
  56. code_muse/command_line/utils.py +105 -0
  57. code_muse/command_line/wiggum_state.py +77 -0
  58. code_muse/config.py +1138 -0
  59. code_muse/config_agent.py +168 -0
  60. code_muse/config_appearance.py +241 -0
  61. code_muse/config_model.py +357 -0
  62. code_muse/config_security.py +73 -0
  63. code_muse/error_logging.py +132 -0
  64. code_muse/evals/__init__.py +35 -0
  65. code_muse/evals/eval_helpers.py +81 -0
  66. code_muse/evals/eval_runner.py +299 -0
  67. code_muse/evals/sample_evals/__init__.py +1 -0
  68. code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
  69. code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
  70. code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
  71. code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
  72. code_muse/fs_scan_cache/__init__.py +31 -0
  73. code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
  74. code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
  75. code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
  76. code_muse/fs_scan_cache/tool_integration.py +309 -0
  77. code_muse/fs_scan_cache/ttl_policy.py +44 -0
  78. code_muse/gemini_code_assist.py +383 -0
  79. code_muse/gemini_model.py +838 -0
  80. code_muse/hook_engine/README.md +105 -0
  81. code_muse/hook_engine/__init__.py +21 -0
  82. code_muse/hook_engine/aliases.py +153 -0
  83. code_muse/hook_engine/engine.py +221 -0
  84. code_muse/hook_engine/executor.py +347 -0
  85. code_muse/hook_engine/matcher.py +154 -0
  86. code_muse/hook_engine/models.py +245 -0
  87. code_muse/hook_engine/registry.py +114 -0
  88. code_muse/hook_engine/trust.py +268 -0
  89. code_muse/hook_engine/validator.py +144 -0
  90. code_muse/http_utils.py +360 -0
  91. code_muse/keymap.py +128 -0
  92. code_muse/list_filtering.py +26 -0
  93. code_muse/main.py +10 -0
  94. code_muse/messaging/__init__.py +259 -0
  95. code_muse/messaging/bus.py +621 -0
  96. code_muse/messaging/commands.py +166 -0
  97. code_muse/messaging/markdown_patches.py +57 -0
  98. code_muse/messaging/message_queue.py +397 -0
  99. code_muse/messaging/messages.py +591 -0
  100. code_muse/messaging/queue_console.py +269 -0
  101. code_muse/messaging/renderers.py +308 -0
  102. code_muse/messaging/rich_renderer.py +1158 -0
  103. code_muse/messaging/shimmer.py +154 -0
  104. code_muse/messaging/spinner/__init__.py +87 -0
  105. code_muse/messaging/spinner/console_spinner.py +250 -0
  106. code_muse/messaging/spinner/spinner_base.py +82 -0
  107. code_muse/messaging/subagent_console.py +458 -0
  108. code_muse/model_factory.py +1203 -0
  109. code_muse/model_switching.py +59 -0
  110. code_muse/model_utils.py +156 -0
  111. code_muse/models.json +66 -0
  112. code_muse/models_cache/__init__.py +26 -0
  113. code_muse/models_cache/blocking_lru_cache.py +98 -0
  114. code_muse/models_cache/cache_writer.py +86 -0
  115. code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
  116. code_muse/models_cache/sha256_hash.pyx +34 -0
  117. code_muse/models_cache/startup_integration.py +75 -0
  118. code_muse/models_dev_api.json +1 -0
  119. code_muse/models_dev_parser.py +590 -0
  120. code_muse/motion.py +126 -0
  121. code_muse/plugins/__init__.py +471 -0
  122. code_muse/plugins/agent_skills/__init__.py +32 -0
  123. code_muse/plugins/agent_skills/config.py +176 -0
  124. code_muse/plugins/agent_skills/discovery.py +309 -0
  125. code_muse/plugins/agent_skills/downloader.py +389 -0
  126. code_muse/plugins/agent_skills/installer.py +19 -0
  127. code_muse/plugins/agent_skills/metadata.py +293 -0
  128. code_muse/plugins/agent_skills/prompt_builder.py +66 -0
  129. code_muse/plugins/agent_skills/register_callbacks.py +298 -0
  130. code_muse/plugins/agent_skills/remote_catalog.py +320 -0
  131. code_muse/plugins/agent_skills/skill_catalog.py +254 -0
  132. code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
  133. code_muse/plugins/agent_skills/skills_menu.py +791 -0
  134. code_muse/plugins/autonomous_memory/__init__.py +39 -0
  135. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
  136. code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
  137. code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
  138. code_muse/plugins/autonomous_memory/consolidation.py +82 -0
  139. code_muse/plugins/autonomous_memory/extraction.py +382 -0
  140. code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
  141. code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
  142. code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
  143. code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
  144. code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
  145. code_muse/plugins/aws_bedrock/__init__.py +14 -0
  146. code_muse/plugins/aws_bedrock/config.py +99 -0
  147. code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
  148. code_muse/plugins/aws_bedrock/utils.py +153 -0
  149. code_muse/plugins/azure_foundry/README.md +238 -0
  150. code_muse/plugins/azure_foundry/__init__.py +15 -0
  151. code_muse/plugins/azure_foundry/config.py +125 -0
  152. code_muse/plugins/azure_foundry/discovery.py +187 -0
  153. code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
  154. code_muse/plugins/azure_foundry/token.py +180 -0
  155. code_muse/plugins/azure_foundry/utils.py +345 -0
  156. code_muse/plugins/build_filter/__init__.py +1 -0
  157. code_muse/plugins/build_filter/register_callbacks.py +201 -0
  158. code_muse/plugins/build_filter/strategies/__init__.py +1 -0
  159. code_muse/plugins/build_filter/strategies/build.py +397 -0
  160. code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
  161. code_muse/plugins/chatgpt_oauth/config.py +52 -0
  162. code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
  163. code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
  164. code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
  165. code_muse/plugins/chatgpt_oauth/utils.py +538 -0
  166. code_muse/plugins/checkpointing/__init__.py +29 -0
  167. code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
  168. code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
  169. code_muse/plugins/checkpointing/register_callbacks.py +51 -0
  170. code_muse/plugins/checkpointing/restore_command.py +263 -0
  171. code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
  172. code_muse/plugins/checkpointing/shadow_git.py +90 -0
  173. code_muse/plugins/claude_code_hooks/__init__.py +1 -0
  174. code_muse/plugins/claude_code_hooks/config.py +188 -0
  175. code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
  176. code_muse/plugins/claude_code_oauth/README.md +167 -0
  177. code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
  178. code_muse/plugins/claude_code_oauth/__init__.py +25 -0
  179. code_muse/plugins/claude_code_oauth/config.py +52 -0
  180. code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
  181. code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
  182. code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
  183. code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
  184. code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
  185. code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
  186. code_muse/plugins/claude_code_oauth/utils.py +664 -0
  187. code_muse/plugins/copilot_auth/__init__.py +11 -0
  188. code_muse/plugins/copilot_auth/config.py +91 -0
  189. code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
  190. code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
  191. code_muse/plugins/copilot_auth/utils.py +584 -0
  192. code_muse/plugins/custom_commands/__init__.py +14 -0
  193. code_muse/plugins/custom_commands/args_injection.py +82 -0
  194. code_muse/plugins/custom_commands/command_discovery.py +89 -0
  195. code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
  196. code_muse/plugins/custom_commands/register_callbacks.py +176 -0
  197. code_muse/plugins/customizable_commands/__init__.py +0 -0
  198. code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
  199. code_muse/plugins/destructive_command_guard/__init__.py +14 -0
  200. code_muse/plugins/destructive_command_guard/detector.py +375 -0
  201. code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
  202. code_muse/plugins/example_custom_command/README.md +280 -0
  203. code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
  204. code_muse/plugins/file_permission_handler/__init__.py +4 -0
  205. code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
  206. code_muse/plugins/filter_engine/__init__.py +30 -0
  207. code_muse/plugins/filter_engine/classifier.py +153 -0
  208. code_muse/plugins/filter_engine/content_detector.py +184 -0
  209. code_muse/plugins/filter_engine/dispatcher.py +244 -0
  210. code_muse/plugins/filter_engine/register_callbacks.py +188 -0
  211. code_muse/plugins/filter_engine/registry.py +279 -0
  212. code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
  213. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
  214. code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
  215. code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
  216. code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
  217. code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
  218. code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
  219. code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
  220. code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
  221. code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
  222. code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
  223. code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
  224. code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
  225. code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
  226. code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
  227. code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
  228. code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
  229. code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
  230. code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
  231. code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
  232. code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
  233. code_muse/plugins/filter_engine/verbosity.py +63 -0
  234. code_muse/plugins/force_push_guard/__init__.py +5 -0
  235. code_muse/plugins/force_push_guard/detector.py +96 -0
  236. code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
  237. code_muse/plugins/force_push_guard/test_detector.py +143 -0
  238. code_muse/plugins/frontend_emitter/__init__.py +25 -0
  239. code_muse/plugins/frontend_emitter/emitter.py +121 -0
  240. code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
  241. code_muse/plugins/gac/__init__.py +4 -0
  242. code_muse/plugins/gac/git_ops.py +136 -0
  243. code_muse/plugins/gac/prompt.py +191 -0
  244. code_muse/plugins/gac/register_callbacks.py +82 -0
  245. code_muse/plugins/hook_creator/__init__.py +1 -0
  246. code_muse/plugins/hook_creator/register_callbacks.py +34 -0
  247. code_muse/plugins/hook_manager/__init__.py +1 -0
  248. code_muse/plugins/hook_manager/config.py +289 -0
  249. code_muse/plugins/hook_manager/hooks_menu.py +563 -0
  250. code_muse/plugins/hook_manager/register_callbacks.py +227 -0
  251. code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
  252. code_muse/plugins/mindpack/__init__.py +0 -0
  253. code_muse/plugins/mindpack/factory.py +930 -0
  254. code_muse/plugins/mindpack/judge.py +573 -0
  255. code_muse/plugins/mindpack/memory.py +100 -0
  256. code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
  257. code_muse/plugins/mindpack/orchestration.py +605 -0
  258. code_muse/plugins/mindpack/register_callbacks.py +175 -0
  259. code_muse/plugins/mindpack/schemas.py +358 -0
  260. code_muse/plugins/mindpack/tools.py +387 -0
  261. code_muse/plugins/oauth_muse_html.py +226 -0
  262. code_muse/plugins/ollama_setup/__init__.py +5 -0
  263. code_muse/plugins/ollama_setup/completer.py +36 -0
  264. code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
  265. code_muse/plugins/plan_command/__init__.py +0 -0
  266. code_muse/plugins/plan_command/register_callbacks.py +206 -0
  267. code_muse/plugins/plan_mode/__init__.py +37 -0
  268. code_muse/plugins/plan_mode/mode_cycling.py +40 -0
  269. code_muse/plugins/plan_mode/plan_generation.py +68 -0
  270. code_muse/plugins/plan_mode/plan_hooks.py +74 -0
  271. code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
  272. code_muse/plugins/plan_mode/register_callbacks.py +121 -0
  273. code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
  274. code_muse/plugins/policy_engine/__init__.py +46 -0
  275. code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
  276. code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
  277. code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
  278. code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
  279. code_muse/plugins/policy_engine/register_callbacks.py +112 -0
  280. code_muse/plugins/pop_command/__init__.py +1 -0
  281. code_muse/plugins/pop_command/register_callbacks.py +189 -0
  282. code_muse/plugins/prompt_newline/__init__.py +13 -0
  283. code_muse/plugins/prompt_newline/config.py +19 -0
  284. code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
  285. code_muse/plugins/safety_status/__init__.py +0 -0
  286. code_muse/plugins/safety_status/register_callbacks.py +113 -0
  287. code_muse/plugins/semantic_compression/__init__.py +6 -0
  288. code_muse/plugins/semantic_compression/compressor.py +295 -0
  289. code_muse/plugins/semantic_compression/config.py +123 -0
  290. code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
  291. code_muse/plugins/shell_minimizer/__init__.py +50 -0
  292. code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
  293. code_muse/plugins/shell_minimizer/pipeline.py +556 -0
  294. code_muse/plugins/shell_minimizer/primitives.py +482 -0
  295. code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
  296. code_muse/plugins/shell_safety/__init__.py +6 -0
  297. code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
  298. code_muse/plugins/shell_safety/command_cache.py +149 -0
  299. code_muse/plugins/shell_safety/register_callbacks.py +202 -0
  300. code_muse/plugins/synthetic_status/__init__.py +1 -0
  301. code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
  302. code_muse/plugins/synthetic_status/status_api.py +145 -0
  303. code_muse/plugins/token_caching/__init__.py +21 -0
  304. code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
  305. code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
  306. code_muse/plugins/token_caching/register_callbacks.py +54 -0
  307. code_muse/plugins/token_caching/stats_display.py +35 -0
  308. code_muse/plugins/token_tracking/__init__.py +26 -0
  309. code_muse/plugins/token_tracking/database.py +381 -0
  310. code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
  311. code_muse/plugins/token_tracking/record.py +55 -0
  312. code_muse/plugins/token_tracking/register_callbacks.py +277 -0
  313. code_muse/plugins/token_tracking/reports.py +329 -0
  314. code_muse/plugins/universal_constructor/__init__.py +13 -0
  315. code_muse/plugins/universal_constructor/models.py +136 -0
  316. code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
  317. code_muse/plugins/universal_constructor/registry.py +390 -0
  318. code_muse/plugins/universal_constructor/runner.py +474 -0
  319. code_muse/plugins/universal_constructor/safety.py +440 -0
  320. code_muse/plugins/universal_constructor/sandbox.py +584 -0
  321. code_muse/provider_identity.py +105 -0
  322. code_muse/pydantic_patches.py +410 -0
  323. code_muse/reopenable_async_client.py +233 -0
  324. code_muse/round_robin_model.py +151 -0
  325. code_muse/secret_storage.py +74 -0
  326. code_muse/security/__init__.py +1 -0
  327. code_muse/security/redaction.cpython-314-darwin.so +0 -0
  328. code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
  329. code_muse/security/redaction.pyx +135 -0
  330. code_muse/session_storage.py +565 -0
  331. code_muse/status_display.py +261 -0
  332. code_muse/stream_parser/__init__.py +76 -0
  333. code_muse/stream_parser/assistant_text_parser.py +90 -0
  334. code_muse/stream_parser/citation_parser.py +76 -0
  335. code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
  336. code_muse/stream_parser/proposed_plan_parser.py +158 -0
  337. code_muse/stream_parser/stream_text_chunk.py +23 -0
  338. code_muse/stream_parser/stream_text_parser.py +27 -0
  339. code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
  340. code_muse/stream_parser/tagged_line_parser.pyx +251 -0
  341. code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
  342. code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
  343. code_muse/summarization_agent.py +308 -0
  344. code_muse/terminal_utils.cpython-314-darwin.so +0 -0
  345. code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
  346. code_muse/terminal_utils.pyx +483 -0
  347. code_muse/tools/__init__.py +459 -0
  348. code_muse/tools/agent_tools.py +613 -0
  349. code_muse/tools/ask_user_question/__init__.py +26 -0
  350. code_muse/tools/ask_user_question/constants.py +73 -0
  351. code_muse/tools/ask_user_question/demo_tui.py +55 -0
  352. code_muse/tools/ask_user_question/handler.py +232 -0
  353. code_muse/tools/ask_user_question/models.py +302 -0
  354. code_muse/tools/ask_user_question/registration.py +37 -0
  355. code_muse/tools/ask_user_question/renderers.py +336 -0
  356. code_muse/tools/ask_user_question/terminal_ui.py +327 -0
  357. code_muse/tools/ask_user_question/theme.py +156 -0
  358. code_muse/tools/ask_user_question/tui_loop.py +422 -0
  359. code_muse/tools/background_jobs.py +99 -0
  360. code_muse/tools/browser/__init__.py +37 -0
  361. code_muse/tools/browser/browser_control.py +289 -0
  362. code_muse/tools/browser/browser_interactions.py +545 -0
  363. code_muse/tools/browser/browser_locators.py +640 -0
  364. code_muse/tools/browser/browser_manager.py +376 -0
  365. code_muse/tools/browser/browser_navigation.py +251 -0
  366. code_muse/tools/browser/browser_screenshot.py +180 -0
  367. code_muse/tools/browser/browser_scripts.py +462 -0
  368. code_muse/tools/browser/browser_workflows.py +222 -0
  369. code_muse/tools/chrome_cdp/__init__.py +1070 -0
  370. code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
  371. code_muse/tools/command_runner.py +1401 -0
  372. code_muse/tools/common.py +1407 -0
  373. code_muse/tools/display.py +87 -0
  374. code_muse/tools/file_modifications.py +1099 -0
  375. code_muse/tools/file_operations.py +860 -0
  376. code_muse/tools/image_tools.py +185 -0
  377. code_muse/tools/meetin_proxy/__init__.py +243 -0
  378. code_muse/tools/meetin_proxy/capture_addon.py +82 -0
  379. code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
  380. code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
  381. code_muse/tools/path_policy.py +219 -0
  382. code_muse/tools/skills_tools.py +586 -0
  383. code_muse/tools/subagent_context.py +158 -0
  384. code_muse/tools/tools_content.py +50 -0
  385. code_muse/tools/universal_constructor.py +965 -0
  386. code_muse/uvx_detection.py +241 -0
  387. code_muse/version_checker.py +86 -0
  388. code_muse-0.0.1.data/data/code_muse/models.json +66 -0
  389. code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
  390. code_muse-0.0.1.dist-info/METADATA +845 -0
  391. code_muse-0.0.1.dist-info/RECORD +394 -0
  392. code_muse-0.0.1.dist-info/WHEEL +4 -0
  393. code_muse-0.0.1.dist-info/entry_points.txt +2 -0
  394. code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,336 @@
1
+ """Rendering functions for the ask_user_question TUI.
2
+
3
+ This module contains the panel rendering logic, separated from the main
4
+ TUI logic to keep files under 600 lines.
5
+ """
6
+
7
+ import io
8
+ import shutil
9
+ from typing import TYPE_CHECKING
10
+
11
+ from prompt_toolkit.formatted_text import ANSI
12
+ from rich.console import Console
13
+ from rich.markup import escape as rich_escape
14
+
15
+ from .constants import (
16
+ ARROW_DOWN,
17
+ ARROW_LEFT,
18
+ ARROW_RIGHT,
19
+ ARROW_UP,
20
+ AUTO_ADD_OTHER_OPTION,
21
+ BORDER_DOUBLE,
22
+ CHECK_MARK,
23
+ CURSOR_POINTER,
24
+ HELP_BORDER_WIDTH,
25
+ MAX_READABLE_WIDTH,
26
+ OTHER_OPTION_DESCRIPTION,
27
+ OTHER_OPTION_LABEL,
28
+ PANEL_CONTENT_PADDING,
29
+ PIPE_SEPARATOR,
30
+ RADIO_FILLED,
31
+ )
32
+ from .theme import get_rich_colors
33
+
34
+ if TYPE_CHECKING:
35
+ from .terminal_ui import QuestionUIState
36
+ from .theme import RichColors
37
+
38
+
39
+ def render_question_panel(
40
+ state: QuestionUIState,
41
+ colors: RichColors | None = None,
42
+ available_width: int | None = None,
43
+ ) -> ANSI:
44
+ """Render the right panel with the current question.
45
+
46
+ Wraps the inner renderer in a guard so a Rich markup error in user-supplied
47
+ text can never doom-loop the TUI (the render callback is invoked on every
48
+ redraw, so an unhandled exception was previously fatal).
49
+
50
+ Args:
51
+ state: The current UI state
52
+ colors: Optional cached RichColors instance. If None, fetches from config.
53
+ """
54
+ try:
55
+ return _render_question_panel_unsafe(state, colors, available_width)
56
+ except Exception as exc: # noqa: BLE001 - intentional: render must never crash
57
+ # Best-effort fallback. Escape everything; never re-enter Rich markup parsing.
58
+ buffer = io.StringIO()
59
+ Console(file=buffer, force_terminal=True, no_color=True).print(
60
+ f"[render error: {type(exc).__name__}: {exc}]",
61
+ markup=False,
62
+ )
63
+ return ANSI(buffer.getvalue())
64
+
65
+
66
+ def _render_question_panel_unsafe(
67
+ state: QuestionUIState,
68
+ colors: RichColors | None,
69
+ available_width: int | None,
70
+ ) -> ANSI:
71
+ """Actual rendering implementation. Caller wraps this in a guard."""
72
+ if colors is None:
73
+ colors = get_rich_colors()
74
+
75
+ buffer = io.StringIO()
76
+ # Use available panel width if provided, otherwise fall back to terminal width
77
+ # Subtract padding to avoid overflow into frame borders
78
+ if available_width is not None:
79
+ terminal_width = min(available_width, MAX_READABLE_WIDTH)
80
+ else:
81
+ terminal_width = min(shutil.get_terminal_size().columns, MAX_READABLE_WIDTH)
82
+ console = Console(
83
+ file=buffer,
84
+ force_terminal=True,
85
+ width=terminal_width,
86
+ legacy_windows=False,
87
+ color_system="truecolor",
88
+ no_color=False,
89
+ force_interactive=True,
90
+ )
91
+
92
+ # Show help overlay if requested
93
+ if state.show_help:
94
+ return _render_help_overlay(console, buffer, colors)
95
+
96
+ question = state.current_question
97
+ q_num = state.current_question_index + 1
98
+ total = len(state.questions)
99
+ pad = PANEL_CONTENT_PADDING # Left padding for visual alignment
100
+
101
+ # Escape all user-supplied strings before they hit Rich markup. Without this,
102
+ # a header like '/foo' becomes '[/foo]' which Rich parses as an unmatched
103
+ # closing tag and raises MarkupError on every redraw (doom-loop).
104
+ # We escape the whole bracketed-decoration so both '[' and ']' are literal.
105
+ safe_header_decoration = rich_escape(f"[{question.header}]")
106
+ safe_question = rich_escape(question.question)
107
+
108
+ # Header
109
+ console.print(
110
+ f"{pad}[{colors.header}]{safe_header_decoration}[/{colors.header}] "
111
+ f"[{colors.progress}]({q_num}/{total})[/{colors.progress}]"
112
+ )
113
+ console.print()
114
+
115
+ # Question text
116
+ if question.multi_select:
117
+ console.print(
118
+ f"{pad}[bold]? {safe_question}[/bold] [dim](select multiple)[/dim]"
119
+ )
120
+ else:
121
+ console.print(f"{pad}[bold]? {safe_question}[/bold]")
122
+ console.print()
123
+
124
+ # Render options
125
+ for i, option in enumerate(question.options):
126
+ _render_option(
127
+ console,
128
+ label=option.label,
129
+ description=option.description,
130
+ is_cursor=state.current_cursor == i,
131
+ is_selected=state.is_option_selected(i),
132
+ multi_select=question.multi_select,
133
+ colors=colors,
134
+ padding=pad,
135
+ )
136
+
137
+ # Render "Other" option if enabled
138
+ if AUTO_ADD_OTHER_OPTION:
139
+ other_idx = len(question.options)
140
+ # Get the stored "Other" text for this question
141
+ other_text = state.get_other_text_for_question(state.current_question_index)
142
+ # Build the description - show stored text if available
143
+ # Escape user input to prevent Rich markup injection
144
+ if other_text:
145
+ other_desc = f'"{rich_escape(other_text)}"'
146
+ else:
147
+ other_desc = OTHER_OPTION_DESCRIPTION
148
+ _render_option(
149
+ console,
150
+ label=OTHER_OPTION_LABEL,
151
+ description=other_desc,
152
+ is_cursor=state.current_cursor == other_idx,
153
+ is_selected=state.is_option_selected(other_idx),
154
+ multi_select=question.multi_select,
155
+ colors=colors,
156
+ padding=pad,
157
+ )
158
+
159
+ # If entering "Other" text, show the input field
160
+ if state.entering_other_text:
161
+ console.print()
162
+ console.print(
163
+ f"{pad}[{colors.input_label}]Enter your custom option:[/{colors.input_label}]"
164
+ )
165
+ console.print(
166
+ f"{pad}[{colors.input_text}]> {state.other_text_buffer}_[/{colors.input_text}]"
167
+ )
168
+ console.print()
169
+ console.print(
170
+ f"{pad}[{colors.input_hint}]Enter to confirm, Esc to cancel[/{colors.input_hint}]"
171
+ )
172
+
173
+ # Help text at bottom - build dynamically, filtering out None entries
174
+ console.print()
175
+ is_last = state.current_question_index == total - 1
176
+ help_parts = [
177
+ "Space Toggle" if question.multi_select else "Space Select",
178
+ "Enter Next" if not is_last else None,
179
+ f"{ARROW_LEFT}{ARROW_RIGHT} Questions" if total > 1 else None,
180
+ "Ctrl+S Submit",
181
+ "? Help",
182
+ ]
183
+ separator = f" {PIPE_SEPARATOR} "
184
+ console.print(
185
+ f"{pad}[{colors.description}]{separator.join(p for p in help_parts if p)}[/{colors.description}]"
186
+ )
187
+
188
+ # Show timeout warning if approaching timeout
189
+ if state.should_show_timeout_warning():
190
+ remaining = state.get_time_remaining()
191
+ console.print()
192
+ console.print(
193
+ f"{pad}[{colors.timeout_warning}]⚠ Timeout in {remaining}s - press any key to continue[/{colors.timeout_warning}]"
194
+ )
195
+
196
+ return ANSI(buffer.getvalue())
197
+
198
+
199
+ # Help overlay shortcut data: (section_name, [(primary_key, alt_key_or_None, description), ...])
200
+ _HELP_SECTIONS: list[tuple[str, list[tuple[str, str | None, str]]]] = [
201
+ (
202
+ "Navigation:",
203
+ [
204
+ (ARROW_UP, "k", "Move up"),
205
+ (ARROW_DOWN, "j", "Move down"),
206
+ (ARROW_LEFT, "h", "Previous question"),
207
+ (ARROW_RIGHT, "l", "Next question"),
208
+ ("g", None, "Jump to first option"),
209
+ ("G", None, "Jump to last option"),
210
+ ],
211
+ ),
212
+ (
213
+ "Selection:",
214
+ [
215
+ ("Space", None, "Select option (radio) / Toggle (checkbox)"),
216
+ ("Enter", None, "Next question (select + advance)"),
217
+ ("a", None, "Select all (multi-select)"),
218
+ ("n", None, "Select none (multi-select)"),
219
+ ("Ctrl+S", None, "Submit all answers"),
220
+ ],
221
+ ),
222
+ (
223
+ "Other:",
224
+ [
225
+ ("Tab", None, "Peek behind (toggle TUI visibility)"),
226
+ ("?", None, "Toggle this help"),
227
+ ("Esc", None, "Cancel"),
228
+ ("Ctrl+C", None, "Cancel"),
229
+ ],
230
+ ),
231
+ ]
232
+
233
+
234
+ def _render_help_overlay(
235
+ console: Console, buffer: io.StringIO, colors: RichColors
236
+ ) -> ANSI:
237
+ """Render the help overlay using data-driven approach."""
238
+ pad = PANEL_CONTENT_PADDING
239
+ border = colors.help_border
240
+ key_style = colors.help_key
241
+ section_style = colors.help_section
242
+
243
+ border_line = f"{pad}[{border}]{BORDER_DOUBLE * HELP_BORDER_WIDTH}[/{border}]"
244
+
245
+ console.print(border_line)
246
+ console.print(
247
+ f"{pad}[{colors.help_title}] KEYBOARD SHORTCUTS[/{colors.help_title}]"
248
+ )
249
+ console.print(border_line)
250
+ console.print()
251
+
252
+ for section_name, shortcuts in _HELP_SECTIONS:
253
+ console.print(f"{pad}[{section_style}]{section_name}[/{section_style}]")
254
+ for primary, alt, desc in shortcuts:
255
+ if alt:
256
+ console.print(
257
+ f"{pad} [{key_style}]{primary}[/{key_style}] / "
258
+ f"[{key_style}]{alt}[/{key_style}] {desc}"
259
+ )
260
+ else:
261
+ console.print(
262
+ f"{pad} [{key_style}]{primary}[/{key_style}] {desc}"
263
+ )
264
+ console.print()
265
+
266
+ console.print(border_line)
267
+ console.print(
268
+ f"{pad}[{colors.help_close}]Press [{key_style}]?[/{key_style}] to close this help[/{colors.help_close}]"
269
+ )
270
+ console.print(border_line)
271
+
272
+ return ANSI(buffer.getvalue())
273
+
274
+
275
+ def _render_option(
276
+ console: Console,
277
+ *,
278
+ label: str,
279
+ description: str,
280
+ is_cursor: bool,
281
+ is_selected: bool,
282
+ multi_select: bool,
283
+ colors: RichColors,
284
+ padding: str = "",
285
+ ) -> None:
286
+ """Render a single option line.
287
+
288
+ Args:
289
+ console: Rich console to render to
290
+ label: Option label text
291
+ description: Option description text
292
+ is_cursor: Whether cursor is on this option
293
+ is_selected: Whether this option is selected
294
+ multi_select: Whether this is a multi-select question
295
+ colors: RichColors instance (required to avoid repeated config lookups)
296
+ padding: Left padding string to prepend to each line
297
+ """
298
+ # Escape label and description to prevent Rich markup injection
299
+ label = rich_escape(label)
300
+ description = rich_escape(description) if description else ""
301
+
302
+ cursor_style = colors.cursor
303
+ selected_style = colors.selected
304
+ desc_style = colors.description
305
+
306
+ # Build the prefix with checkbox or radio button
307
+ if multi_select:
308
+ # Checkbox style: [✓] or [ ]
309
+ checkbox = f"[{CHECK_MARK}]" if is_selected else "[ ]"
310
+ if is_cursor:
311
+ prefix = f"[{cursor_style}]{CURSOR_POINTER} {checkbox}[/{cursor_style}]"
312
+ else:
313
+ prefix = f" {checkbox}"
314
+ else:
315
+ # Radio button style: (●) or ( )
316
+ radio = f"({RADIO_FILLED})" if is_selected else "( )"
317
+ if is_cursor:
318
+ prefix = f"[{cursor_style}]{CURSOR_POINTER} {radio}[/{cursor_style}]"
319
+ else:
320
+ prefix = f" {radio}"
321
+
322
+ # Build the label
323
+ if is_cursor:
324
+ label_styled = f"[{cursor_style}]{label}[/{cursor_style}]"
325
+ elif is_selected:
326
+ label_styled = f"[{selected_style}]{label}[/{selected_style}]"
327
+ else:
328
+ label_styled = label
329
+
330
+ # Print option
331
+ console.print(f"{padding} {prefix} {label_styled}")
332
+
333
+ # Print description if present
334
+ if description:
335
+ console.print(f"{padding} [{desc_style}]{description}[/{desc_style}]")
336
+ console.print()
@@ -0,0 +1,327 @@
1
+ """Terminal UI for ask_user_question tool.
2
+
3
+ Uses prompt_toolkit for a split-panel TUI similar to the /colors command.
4
+ Left panel (20%): Question headers/tabs
5
+ Right panel (80%): Current question with options
6
+
7
+ Navigation:
8
+ - Left/Right: Switch between questions
9
+ - Up/Down: Navigate options within current question
10
+ - Enter: Select option (single-select) or confirm (multi-select)
11
+ - Space: Toggle option (multi-select only)
12
+ - Esc/Ctrl+C: Cancel
13
+ """
14
+
15
+ import time
16
+
17
+ from .constants import (
18
+ AUTO_ADD_OTHER_OPTION,
19
+ DEFAULT_TIMEOUT_SECONDS,
20
+ LEFT_PANEL_PADDING,
21
+ MAX_LEFT_PANEL_WIDTH,
22
+ MIN_LEFT_PANEL_WIDTH,
23
+ OTHER_OPTION_LABEL,
24
+ TIMEOUT_WARNING_SECONDS,
25
+ )
26
+ from .models import Question, QuestionAnswer
27
+
28
+
29
+ class CancelledException(Exception):
30
+ """Raised when user cancels the interaction."""
31
+
32
+
33
+ class QuestionUIState:
34
+ """Holds the current UI state for the question interaction."""
35
+
36
+ def __init__(self, questions: list[Question]) -> None:
37
+ """Initialize state with questions.
38
+
39
+ Args:
40
+ questions: List of validated Question objects
41
+ """
42
+ self.questions = questions
43
+ self.current_question_index = 0
44
+ # For each question, track: cursor position and selected options
45
+ self.cursor_positions: list[int] = [0] * len(questions)
46
+ # For multi-select, track selected option indices per question
47
+ self.selected_options: list[set[int]] = [set() for _ in questions]
48
+ # For single-select, track the selected option index per question (None = not selected)
49
+ self.single_selections: list[int | None] = [None] * len(questions)
50
+ # Store "Other" text per question
51
+ self.other_texts: list[str | None] = [None] * len(questions)
52
+ # Track if we're in "Other" text input mode
53
+ self.entering_other_text = False
54
+ self.other_text_buffer = ""
55
+ # Track if help overlay is shown
56
+ self.show_help = False
57
+ # Timeout tracking (use monotonic to avoid clock drift/NTP issues)
58
+ self.timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS
59
+ self.last_activity_time: float = time.monotonic()
60
+
61
+ def reset_activity_timer(self) -> None:
62
+ """Reset the activity timer (called on user input)."""
63
+ self.last_activity_time = time.monotonic()
64
+
65
+ def get_time_remaining(self) -> int:
66
+ """Get seconds remaining before timeout."""
67
+ elapsed = time.monotonic() - self.last_activity_time
68
+ remaining = self.timeout_seconds - elapsed
69
+ return max(0, int(remaining))
70
+
71
+ def is_timed_out(self) -> bool:
72
+ """Check if the interaction has timed out."""
73
+ return self.get_time_remaining() <= 0
74
+
75
+ def should_show_timeout_warning(self) -> bool:
76
+ """Check if we should show the timeout warning."""
77
+ remaining = self.get_time_remaining()
78
+ return remaining <= TIMEOUT_WARNING_SECONDS and remaining > 0
79
+
80
+ @property
81
+ def current_question(self) -> Question:
82
+ """Get the currently displayed question."""
83
+ return self.questions[self.current_question_index]
84
+
85
+ def get_left_panel_width(self) -> int:
86
+ """Calculate the left panel width based on longest header.
87
+
88
+ Returns:
89
+ Width in characters, including padding for cursor and checkmark.
90
+ """
91
+ max_header_len = max(len(q.header) for q in self.questions)
92
+ width = max_header_len + LEFT_PANEL_PADDING
93
+ return max(MIN_LEFT_PANEL_WIDTH, min(width, MAX_LEFT_PANEL_WIDTH))
94
+
95
+ def get_other_text_for_question(self, index: int) -> str | None:
96
+ """Get the 'Other' text for a specific question.
97
+
98
+ Args:
99
+ index: Question index
100
+
101
+ Returns:
102
+ The stored other_text or None if not set.
103
+ """
104
+ return self.other_texts[index]
105
+
106
+ def jump_to_first(self) -> None:
107
+ """Jump cursor to first option."""
108
+ self.current_cursor = 0
109
+
110
+ def jump_to_last(self) -> None:
111
+ """Jump cursor to last option."""
112
+ self.current_cursor = self.total_options - 1
113
+
114
+ @property
115
+ def current_cursor(self) -> int:
116
+ """Get cursor position for current question."""
117
+ return self.cursor_positions[self.current_question_index]
118
+
119
+ @current_cursor.setter
120
+ def current_cursor(self, value: int) -> None:
121
+ """Set cursor position for current question."""
122
+ self.cursor_positions[self.current_question_index] = value
123
+
124
+ @property
125
+ def total_options(self) -> int:
126
+ """Get total number of options including 'Other' if enabled."""
127
+ count = len(self.current_question.options)
128
+ if AUTO_ADD_OTHER_OPTION:
129
+ count += 1
130
+ return count
131
+
132
+ def is_question_answered(self, index: int) -> bool:
133
+ """Check if a question has at least one selection.
134
+
135
+ For multi-select: True if any option is selected or Other text provided.
136
+ For single-select: True if an option is selected.
137
+ """
138
+ question = self.questions[index]
139
+ if question.multi_select:
140
+ return (
141
+ len(self.selected_options[index]) > 0
142
+ or self.other_texts[index] is not None
143
+ )
144
+ return self.single_selections[index] is not None
145
+
146
+ def is_other_option(self, index: int) -> bool:
147
+ """Check if the given index is the 'Other' option."""
148
+ if not AUTO_ADD_OTHER_OPTION:
149
+ return False
150
+ return index == len(self.current_question.options)
151
+
152
+ def enter_other_text_mode(self) -> None:
153
+ """Enter text input mode for the 'Other' option.
154
+
155
+ This centralizes the logic for starting 'Other' text entry,
156
+ avoiding duplication in the keyboard handlers.
157
+ """
158
+ self.entering_other_text = True
159
+ self.other_text_buffer = self.other_texts[self.current_question_index] or ""
160
+
161
+ def commit_other_text(self) -> None:
162
+ """Save the other text buffer and mark the Other option as selected.
163
+
164
+ This centralizes the logic for confirming an 'Other' text entry,
165
+ avoiding duplication in the various keyboard handlers.
166
+ """
167
+ if not self.other_text_buffer.strip():
168
+ # Don't save empty/whitespace-only text
169
+ self.entering_other_text = False
170
+ self.other_text_buffer = ""
171
+ return
172
+
173
+ self.other_texts[self.current_question_index] = self.other_text_buffer
174
+ other_idx = len(self.current_question.options)
175
+ self._select_option_at(self.current_question_index, other_idx)
176
+ self.entering_other_text = False
177
+ self.other_text_buffer = ""
178
+
179
+ def _select_option_at(self, question_idx: int, option_idx: int) -> None:
180
+ """Mark an option as selected for the given question.
181
+
182
+ Handles both single-select and multi-select modes.
183
+ """
184
+ if self.questions[question_idx].multi_select:
185
+ self.selected_options[question_idx].add(option_idx)
186
+ else:
187
+ self.single_selections[question_idx] = option_idx
188
+
189
+ def select_all_options(self) -> None:
190
+ """Select all regular options for the current question (multi-select only)."""
191
+ if not self.current_question.multi_select:
192
+ return
193
+ for i in range(len(self.current_question.options)):
194
+ self.selected_options[self.current_question_index].add(i)
195
+
196
+ def select_no_options(self) -> None:
197
+ """Clear all selections for the current question (multi-select only)."""
198
+ if not self.current_question.multi_select:
199
+ return
200
+ self.selected_options[self.current_question_index].clear()
201
+ self.other_texts[self.current_question_index] = None
202
+
203
+ def move_cursor_up(self) -> None:
204
+ """Move cursor up within current question."""
205
+ if self.current_cursor > 0:
206
+ self.current_cursor -= 1
207
+
208
+ def move_cursor_down(self) -> None:
209
+ """Move cursor down within current question."""
210
+ if self.current_cursor < self.total_options - 1:
211
+ self.current_cursor += 1
212
+
213
+ def next_question(self) -> None:
214
+ """Move to next question."""
215
+ if self.current_question_index < len(self.questions) - 1:
216
+ self.current_question_index += 1
217
+
218
+ def prev_question(self) -> None:
219
+ """Move to previous question."""
220
+ if self.current_question_index > 0:
221
+ self.current_question_index -= 1
222
+
223
+ def toggle_current_option(self) -> None:
224
+ """Toggle the current option for multi-select questions."""
225
+ if not self.current_question.multi_select:
226
+ return
227
+ cursor = self.current_cursor
228
+ selected = self.selected_options[self.current_question_index]
229
+ if cursor in selected:
230
+ selected.discard(cursor)
231
+ else:
232
+ selected.add(cursor)
233
+
234
+ def select_current_option(self) -> None:
235
+ """Select current option for single-select questions."""
236
+ if self.current_question.multi_select:
237
+ return
238
+ self.single_selections[self.current_question_index] = self.current_cursor
239
+
240
+ def is_option_selected(self, index: int) -> bool:
241
+ """Check if an option is selected."""
242
+ if self.current_question.multi_select:
243
+ return index in self.selected_options[self.current_question_index]
244
+ else:
245
+ return self.single_selections[self.current_question_index] == index
246
+
247
+ def _resolve_option_label(
248
+ self, question: Question, question_idx: int, opt_idx: int
249
+ ) -> tuple[str, str | None]:
250
+ """Resolve the label and other_text for an option index.
251
+
252
+ Args:
253
+ question: The question being answered
254
+ question_idx: Index of the question in self.questions
255
+ opt_idx: Index of the selected option
256
+
257
+ Returns:
258
+ Tuple of (label, other_text) where other_text is set only for "Other" option
259
+ """
260
+ if AUTO_ADD_OTHER_OPTION and opt_idx == len(question.options):
261
+ return OTHER_OPTION_LABEL, self.other_texts[question_idx]
262
+ return question.options[opt_idx].label, None
263
+
264
+ def build_answers(self) -> list[QuestionAnswer]:
265
+ """Build the list of answers from current state."""
266
+ answers = []
267
+ for i, question in enumerate(self.questions):
268
+ selected_labels: list[str] = []
269
+ other_text: str | None = None
270
+
271
+ if question.multi_select:
272
+ # Multi-select: gather all selected option labels
273
+ for opt_idx in sorted(self.selected_options[i]):
274
+ label, opt_other = self._resolve_option_label(question, i, opt_idx)
275
+ selected_labels.append(label)
276
+ if opt_other is not None:
277
+ other_text = opt_other
278
+ else:
279
+ # Single-select: get the selected option
280
+ sel_idx = self.single_selections[i]
281
+ if sel_idx is not None:
282
+ label, other_text = self._resolve_option_label(question, i, sel_idx)
283
+ selected_labels.append(label)
284
+
285
+ answers.append(
286
+ QuestionAnswer(
287
+ question_header=question.header,
288
+ selected_options=selected_labels,
289
+ other_text=other_text,
290
+ )
291
+ )
292
+ return answers
293
+
294
+
295
+ async def interactive_question_picker(
296
+ questions: list[Question],
297
+ timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
298
+ ) -> tuple[list[QuestionAnswer], bool, bool]:
299
+ """Show an interactive split-panel TUI for questions.
300
+
301
+ Args:
302
+ questions: List of validated Question objects
303
+ timeout_seconds: Inactivity timeout in seconds
304
+
305
+ Returns:
306
+ Tuple of (answers, cancelled, timed_out) where:
307
+ - answers: List of QuestionAnswer objects
308
+ - cancelled: True if user cancelled
309
+ - timed_out: True if interaction timed out
310
+
311
+ Raises:
312
+ CancelledException: If user cancels with Esc/Ctrl+C
313
+ """
314
+ # Import here to avoid circular dependency with command_runner
315
+ from code_muse.tools.command_runner import set_awaiting_user_input
316
+
317
+ state = QuestionUIState(questions)
318
+ state.timeout_seconds = timeout_seconds
319
+ set_awaiting_user_input(True)
320
+
321
+ try:
322
+ from .tui_loop import run_question_tui
323
+
324
+ # prompt_toolkit manages alt screen via full_screen=True
325
+ return await run_question_tui(state)
326
+ finally:
327
+ set_awaiting_user_input(False)