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,709 @@
1
+ """Interactive terminal UI for loading autosave sessions.
2
+
3
+ Provides a beautiful split-panel interface for browsing and loading
4
+ autosave sessions with live preview of message content.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import sys
10
+ from datetime import datetime
11
+ from io import StringIO
12
+ from pathlib import Path
13
+
14
+ from prompt_toolkit.application import Application
15
+ from prompt_toolkit.key_binding import KeyBindings
16
+ from prompt_toolkit.layout import Dimension, Layout, VSplit, Window
17
+ from prompt_toolkit.layout.controls import FormattedTextControl
18
+ from prompt_toolkit.widgets import Frame
19
+ from rich.console import Console
20
+ from rich.markdown import Markdown
21
+
22
+ from code_muse.command_line.pagination import (
23
+ ensure_visible_page,
24
+ get_page_bounds,
25
+ get_total_pages,
26
+ )
27
+ from code_muse.config import AUTOSAVE_DIR
28
+ from code_muse.session_storage import list_sessions, load_session
29
+ from code_muse.tools.command_runner import set_awaiting_user_input
30
+
31
+ PAGE_SIZE = 15 # Sessions per page
32
+
33
+
34
+ def _get_session_metadata(base_dir: Path, session_name: str) -> dict:
35
+ """Load metadata for a session."""
36
+ meta_path = base_dir / f"{session_name}_meta.json"
37
+ try:
38
+ with meta_path.open("r", encoding="utf-8") as f:
39
+ return json.load(f)
40
+ except Exception:
41
+ return {}
42
+
43
+
44
+ def _get_session_entries(base_dir: Path) -> list[tuple[str, dict]]:
45
+ """Get all sessions with their metadata, sorted by timestamp."""
46
+ try:
47
+ sessions = list_sessions(base_dir)
48
+ except FileNotFoundError, PermissionError:
49
+ return []
50
+
51
+ entries = []
52
+
53
+ for name in sessions:
54
+ try:
55
+ metadata = _get_session_metadata(base_dir, name)
56
+ except FileNotFoundError, PermissionError:
57
+ metadata = {}
58
+ entries.append((name, metadata))
59
+
60
+ # Sort by timestamp (most recent first)
61
+ def sort_key(entry):
62
+ _, metadata = entry
63
+ timestamp = metadata.get("timestamp")
64
+ if timestamp:
65
+ try:
66
+ return datetime.fromisoformat(timestamp)
67
+ except ValueError:
68
+ return datetime.min
69
+ return datetime.min
70
+
71
+ entries.sort(key=sort_key, reverse=True)
72
+ return entries
73
+
74
+
75
+ def _extract_last_user_message(history: list) -> str:
76
+ """Extract the most recent user message from history.
77
+
78
+ Joins all content parts from the message since messages can have
79
+ multiple parts (e.g., text + attachments, multi-part prompts).
80
+ """
81
+ # Walk backwards through history to find last user message
82
+ for msg in reversed(history):
83
+ content_parts = []
84
+ for part in msg.parts:
85
+ if hasattr(part, "content"):
86
+ content = part.content
87
+ if isinstance(content, str) and content.strip():
88
+ content_parts.append(content)
89
+ if content_parts:
90
+ return "\n\n".join(content_parts)
91
+ return "[No messages found]"
92
+
93
+
94
+ def _extract_message_content(msg) -> tuple[str, str]:
95
+ """Extract role and content from a message.
96
+
97
+ Returns:
98
+ Tuple of (role, content) where role is 'user', 'assistant', or 'tool'
99
+ """
100
+ # Determine role based on message kind AND part types
101
+ # tool-return comes in a 'request' message but it's not from the user
102
+ part_kinds = [getattr(p, "part_kind", "unknown") for p in msg.parts]
103
+
104
+ if msg.kind == "request":
105
+ # Check if this is a tool return (not actually user input)
106
+ role = "tool" if all(pk == "tool-return" for pk in part_kinds) else "user"
107
+ else:
108
+ # Response from assistant
109
+ role = "tool" if all(pk == "tool-call" for pk in part_kinds) else "assistant"
110
+
111
+ # Extract content from parts, handling different part types
112
+ content_parts = []
113
+ for part in msg.parts:
114
+ part_kind = getattr(part, "part_kind", "unknown")
115
+
116
+ if part_kind == "tool-call":
117
+ # Assistant is calling a tool - show tool name and args preview
118
+ tool_name = getattr(part, "tool_name", "unknown")
119
+ args = getattr(part, "args", {})
120
+ # Create a condensed args preview
121
+ if args:
122
+ args_preview = str(args)[:100]
123
+ if len(str(args)) > 100:
124
+ args_preview += "..."
125
+ content_parts.append(
126
+ f"🔧 Tool Call: {tool_name}\n Args: {args_preview}"
127
+ )
128
+ else:
129
+ content_parts.append(f"🔧 Tool Call: {tool_name}")
130
+
131
+ elif part_kind == "tool-return":
132
+ # Tool result being returned - show tool name and truncated result
133
+ tool_name = getattr(part, "tool_name", "unknown")
134
+ result = getattr(part, "content", "")
135
+ if isinstance(result, str) and result.strip():
136
+ # Truncate long results
137
+ preview = result[:200].replace("\n", " ")
138
+ if len(result) > 200:
139
+ preview += "..."
140
+ content_parts.append(f"📥 Tool Result: {tool_name}\n {preview}")
141
+ else:
142
+ content_parts.append(f"📥 Tool Result: {tool_name}")
143
+
144
+ elif hasattr(part, "content"):
145
+ # Regular text content (user-prompt, text, thinking, etc.)
146
+ content = part.content
147
+ if isinstance(content, str) and content.strip():
148
+ content_parts.append(content)
149
+
150
+ content = "\n\n".join(content_parts) if content_parts else "[No content]"
151
+ return role, content
152
+
153
+
154
+ def _render_menu_panel(
155
+ entries: list[tuple[str, dict]],
156
+ page: int,
157
+ selected_idx: int,
158
+ browse_mode: bool = False,
159
+ ) -> list:
160
+ """Render the left menu panel with pagination."""
161
+ lines = []
162
+ total_pages = get_total_pages(len(entries), PAGE_SIZE)
163
+ start_idx, end_idx = get_page_bounds(page, len(entries), PAGE_SIZE)
164
+
165
+ lines.append(("", f" Session Page(s): ({page + 1}/{total_pages})"))
166
+ lines.append(("", "\n\n"))
167
+
168
+ if not entries:
169
+ lines.append(("fg:yellow", " No autosave sessions found."))
170
+ lines.append(("", "\n\n"))
171
+ # Navigation hints (always show)
172
+ lines.append(("", "\n"))
173
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
174
+ lines.append(("", "Navigate\n"))
175
+ lines.append(("fg:ansibrightblack", " ←/→ "))
176
+ lines.append(("", "Page\n"))
177
+ lines.append(("fg:green", " Enter "))
178
+ lines.append(("", "Load\n"))
179
+ lines.append(("fg:ansibrightred", " Ctrl+C "))
180
+ lines.append(("", "Cancel"))
181
+ return lines
182
+
183
+ # Show sessions for current page
184
+ for i in range(start_idx, end_idx):
185
+ session_name, metadata = entries[i]
186
+ is_selected = i == selected_idx
187
+
188
+ # Format timestamp
189
+ timestamp = metadata.get("timestamp", "unknown")
190
+ try:
191
+ dt = datetime.fromisoformat(timestamp)
192
+ time_str = dt.strftime("%Y-%m-%d %H:%M")
193
+ except Exception:
194
+ time_str = "unknown time"
195
+
196
+ # Format message count
197
+ msg_count = metadata.get("message_count", "?")
198
+
199
+ # Highlight selected item
200
+ if is_selected:
201
+ lines.append(("fg:ansibrightblack", f" > {time_str} • {msg_count} msgs"))
202
+ else:
203
+ lines.append(("fg:ansibrightblack", f" {time_str} • {msg_count} msgs"))
204
+
205
+ lines.append(("", "\n"))
206
+
207
+ # Navigation hints - change based on browse mode
208
+ lines.append(("", "\n"))
209
+ if browse_mode:
210
+ lines.append(("fg:ansicyan", " ↑/↓ "))
211
+ lines.append(("", "Browse msgs\n"))
212
+ lines.append(("fg:ansiyellow", " Esc "))
213
+ lines.append(("", "Exit browser\n"))
214
+ else:
215
+ lines.append(("fg:ansibrightblack", " ↑/↓ "))
216
+ lines.append(("", "Navigate\n"))
217
+ lines.append(("fg:ansibrightblack", " ←/→ "))
218
+ lines.append(("", "Page\n"))
219
+ lines.append(("fg:ansicyan", " e "))
220
+ lines.append(("", "Browse msgs\n"))
221
+ lines.append(("fg:green", " Enter "))
222
+ lines.append(("", "Load\n"))
223
+ lines.append(("fg:ansibrightred", " Ctrl+C "))
224
+ lines.append(("", "Cancel"))
225
+
226
+ return lines
227
+
228
+
229
+ def _render_message_browser_panel(
230
+ history: list,
231
+ message_idx: int,
232
+ session_name: str,
233
+ ) -> list:
234
+ """Render the message browser panel showing a single message.
235
+
236
+ Args:
237
+ history: Full message history list
238
+ message_idx: Index into history (0 = most recent)
239
+ session_name: Name of the session being browsed
240
+ """
241
+ lines = []
242
+
243
+ lines.append(("fg:ansicyan bold", " MESSAGE BROWSER"))
244
+ lines.append(("", "\n\n"))
245
+
246
+ total_messages = len(history)
247
+ if total_messages == 0:
248
+ lines.append(("fg:yellow", " No messages in this session."))
249
+ lines.append(("", "\n"))
250
+ return lines
251
+
252
+ # Clamp index to valid range
253
+ message_idx = max(0, min(message_idx, total_messages - 1))
254
+
255
+ # Get message (reverse index so 0 = most recent)
256
+ actual_idx = total_messages - 1 - message_idx
257
+ msg = history[actual_idx]
258
+
259
+ # Extract role and content
260
+ role, content = _extract_message_content(msg)
261
+
262
+ # Session info
263
+ lines.append(("fg:ansibrightblack", f" Session: {session_name}"))
264
+ lines.append(("", "\n"))
265
+
266
+ # Message position indicator
267
+ display_num = message_idx + 1 # 1-based for display
268
+ lines.append(("bold", f" Message {display_num} of {total_messages}"))
269
+ lines.append(("", "\n\n"))
270
+
271
+ # Role indicator with icon and color
272
+ if role == "user":
273
+ lines.append(("fg:ansicyan bold", " 🧑 USER"))
274
+ elif role == "tool":
275
+ lines.append(("fg:ansiyellow bold", " 🔧 TOOL"))
276
+ else:
277
+ lines.append(("fg:ansigreen bold", " 🤖 ASSISTANT"))
278
+ lines.append(("", "\n"))
279
+
280
+ # Separator line
281
+ lines.append(("fg:ansibrightblack", " " + "─" * 40))
282
+ lines.append(("", "\n"))
283
+
284
+ # Render content - use markdown for user/assistant, plain text for tool
285
+ try:
286
+ if role == "tool":
287
+ # Tool messages are already formatted, don't pass through markdown
288
+ # Use yellow color for tool output
289
+ rendered = content
290
+ text_color = "fg:ansiyellow"
291
+ else:
292
+ # User and assistant messages should be rendered as markdown
293
+ # Rich will handle the styling via ANSI codes
294
+ console = Console(
295
+ file=StringIO(),
296
+ legacy_windows=False,
297
+ no_color=False,
298
+ force_terminal=False,
299
+ width=72,
300
+ )
301
+ md = Markdown(content)
302
+ console.print(md)
303
+ rendered = console.file.getvalue()
304
+ # Don't override Rich's ANSI styling - use empty style
305
+ text_color = ""
306
+
307
+ # Show full message without truncation
308
+ message_lines = rendered.split("\n")
309
+
310
+ for line in message_lines:
311
+ lines.append((text_color, f" {line}"))
312
+ lines.append(("", "\n"))
313
+
314
+ except Exception as e:
315
+ lines.append(("fg:red", f" Error rendering message: {e}"))
316
+ lines.append(("", "\n"))
317
+
318
+ # Navigation hint at bottom
319
+ lines.append(("", "\n"))
320
+ lines.append(("fg:ansibrightblack", " ↑ older ↓ newer Esc exit"))
321
+ lines.append(("", "\n"))
322
+
323
+ return lines
324
+
325
+
326
+ def _render_preview_panel(base_dir: Path, entry: tuple[str, dict | None]) -> list:
327
+ """Render the right preview panel with message content using rich markdown."""
328
+ lines = []
329
+
330
+ lines.append(("dim cyan", " PREVIEW"))
331
+ lines.append(("", "\n\n"))
332
+
333
+ if not entry:
334
+ lines.append(("fg:yellow", " No session selected."))
335
+ lines.append(("", "\n"))
336
+ return lines
337
+
338
+ session_name, metadata = entry
339
+
340
+ # Show metadata
341
+ lines.append(("bold", " Session: "))
342
+ lines.append(("", session_name))
343
+ lines.append(("", "\n"))
344
+
345
+ timestamp = metadata.get("timestamp", "unknown")
346
+ try:
347
+ dt = datetime.fromisoformat(timestamp)
348
+ time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
349
+ except Exception:
350
+ time_str = timestamp
351
+ lines.append(("fg:ansibrightblack", f" Saved: {time_str}"))
352
+ lines.append(("", "\n"))
353
+
354
+ msg_count = metadata.get("message_count", 0)
355
+ tokens = metadata.get("total_tokens", 0)
356
+ lines.append(
357
+ ("fg:ansibrightblack", f" Messages: {msg_count} • Tokens: {tokens:,}")
358
+ )
359
+ lines.append(("", "\n\n"))
360
+
361
+ lines.append(("bold", " Last Message:"))
362
+ lines.append(("fg:ansibrightblack", " (press 'e' to browse full history)"))
363
+ lines.append(("", "\n"))
364
+
365
+ # Try to load and preview the last message
366
+ try:
367
+ history = load_session(session_name, base_dir)
368
+ last_message = _extract_last_user_message(history)
369
+
370
+ # Render markdown with rich
371
+ console = Console(
372
+ file=StringIO(),
373
+ legacy_windows=False,
374
+ no_color=False,
375
+ force_terminal=False,
376
+ width=76,
377
+ )
378
+ md = Markdown(last_message)
379
+ console.print(md)
380
+ rendered = console.file.getvalue()
381
+
382
+ # Show full message without truncation
383
+ message_lines = rendered.split("\n")
384
+
385
+ for line in message_lines:
386
+ # Rich already rendered the markdown, just display it dimmed
387
+ lines.append(("fg:ansibrightblack", f" {line}"))
388
+ lines.append(("", "\n"))
389
+
390
+ except Exception as e:
391
+ lines.append(("fg:red", f" Error loading preview: {e}"))
392
+ lines.append(("", "\n"))
393
+
394
+ return lines
395
+
396
+
397
+ # Default number of messages to display when resuming a session
398
+ # This is overridden by the user config 'resume_message_count'
399
+ DEFAULT_RESUME_DISPLAY_COUNT = 50
400
+
401
+
402
+ def display_resumed_history(
403
+ history: list,
404
+ num_messages: int | None = None,
405
+ ) -> None:
406
+ """Display recent message history after resuming a session.
407
+
408
+ Shows the last N messages from the conversation so users have context
409
+ about where they left off. Uses the same rendering style as normal chat.
410
+
411
+ Args:
412
+ history: The full message history list
413
+ num_messages: Number of messages to display. If None, uses the
414
+ 'resume_message_count' config value (default 50).
415
+ Configurable via: /set resume_message_count=50
416
+ """
417
+ from rich.console import Console
418
+ from rich.markdown import Markdown
419
+ from rich.rule import Rule
420
+
421
+ from code_muse.config import get_banner_color, get_resume_message_count
422
+
423
+ if not history:
424
+ return
425
+
426
+ # Use config value if num_messages not explicitly provided
427
+ if num_messages is None:
428
+ num_messages = get_resume_message_count()
429
+
430
+ console = Console()
431
+ total_messages = len(history)
432
+
433
+ # Skip if only system message exists
434
+ if total_messages <= 1:
435
+ return
436
+
437
+ # Determine which messages to show (skip first system message)
438
+ # We want to show the last N non-system messages
439
+ displayable_history = history[1:] # Skip system message
440
+ total_displayable = len(displayable_history)
441
+
442
+ if total_displayable == 0:
443
+ return
444
+
445
+ messages_to_show = (
446
+ displayable_history[-num_messages:]
447
+ if total_displayable > num_messages
448
+ else displayable_history
449
+ )
450
+ hidden_count = total_displayable - len(messages_to_show)
451
+
452
+ # Print header with hidden count if applicable
453
+ console.print()
454
+ if hidden_count > 0:
455
+ console.print(
456
+ Rule(
457
+ f"{hidden_count} earlier messages",
458
+ style="dim",
459
+ )
460
+ )
461
+ console.print()
462
+
463
+ # Get banner color for agent responses
464
+ response_color = get_banner_color("agent_response")
465
+
466
+ # Render each message in the same style as normal chat
467
+ for msg in messages_to_show:
468
+ role, content = _extract_message_content(msg)
469
+
470
+ # Print banner matching normal chat style
471
+ if role == "user":
472
+ # User messages don't have a banner in normal chat,
473
+ # but we add one for clarity in resumed history
474
+ console.print("[dim]> [/dim]", end="")
475
+ console.print(f"[bold]{content}[/bold]")
476
+ elif role == "tool":
477
+ # Tool output is typically dim/collapsed
478
+ console.print(f"[dim]{content}[/dim]")
479
+ else: # assistant
480
+ # Use the exact same banner format as normal AGENT RESPONSE
481
+ banner = f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
482
+ console.print(f"\n{banner}")
483
+ # Render content as markdown (same as normal chat)
484
+ md = Markdown(content)
485
+ console.print(md)
486
+
487
+ console.print() # Blank line between messages
488
+
489
+ # Print footer separator
490
+ console.print(Rule("Session Resumed", style="bold green"))
491
+ console.print()
492
+
493
+
494
+ async def interactive_autosave_picker() -> str | None:
495
+ """Show interactive terminal UI to select an autosave session.
496
+
497
+ Returns:
498
+ Session name to load, or None if cancelled
499
+ """
500
+ base_dir = Path(AUTOSAVE_DIR)
501
+ entries = _get_session_entries(base_dir)
502
+
503
+ if not entries:
504
+ from code_muse.messaging import emit_info
505
+
506
+ emit_info("No autosave sessions found.")
507
+ return None
508
+
509
+ # State
510
+ selected_idx = [0] # Current selection (global index)
511
+ current_page = [0] # Current page
512
+ result = [None] # Selected session name
513
+
514
+ # Browse mode state
515
+ browse_mode = [False] # Are we browsing messages within a session?
516
+ message_idx = [0] # Current message index (0 = most recent)
517
+ cached_history = [None] # Cached history for current session in browse mode
518
+
519
+ total_pages = get_total_pages(len(entries), PAGE_SIZE)
520
+
521
+ def get_current_entry() -> tuple[str, dict | None]:
522
+ if 0 <= selected_idx[0] < len(entries):
523
+ return entries[selected_idx[0]]
524
+ return None
525
+
526
+ # Build UI
527
+ menu_control = FormattedTextControl(text="")
528
+ preview_control = FormattedTextControl(text="")
529
+
530
+ def update_display():
531
+ """Update both panels."""
532
+ menu_control.text = _render_menu_panel(
533
+ entries, current_page[0], selected_idx[0], browse_mode[0]
534
+ )
535
+ # Show message browser if in browse mode, otherwise show preview
536
+ if browse_mode[0] and cached_history[0] is not None:
537
+ entry = get_current_entry()
538
+ session_name = entry[0] if entry else "unknown"
539
+ preview_control.text = _render_message_browser_panel(
540
+ cached_history[0], message_idx[0], session_name
541
+ )
542
+ else:
543
+ preview_control.text = _render_preview_panel(base_dir, get_current_entry())
544
+
545
+ menu_window = Window(
546
+ content=menu_control, wrap_lines=True, width=Dimension(weight=30)
547
+ )
548
+ preview_window = Window(
549
+ content=preview_control, wrap_lines=True, width=Dimension(weight=70)
550
+ )
551
+
552
+ menu_frame = Frame(menu_window, width=Dimension(weight=30), title="Sessions")
553
+ preview_frame = Frame(preview_window, width=Dimension(weight=70), title="Preview")
554
+
555
+ # Make left panel narrower (15% vs 85%)
556
+ root_container = VSplit(
557
+ [
558
+ menu_frame,
559
+ preview_frame,
560
+ ]
561
+ )
562
+
563
+ # Key bindings
564
+ kb = KeyBindings()
565
+
566
+ @kb.add("up")
567
+ @kb.add("c-p") # Ctrl+P = previous (Emacs-style)
568
+ def _(event):
569
+ if browse_mode[0]:
570
+ # In browse mode: go to older message
571
+ if cached_history[0] and message_idx[0] < len(cached_history[0]) - 1:
572
+ message_idx[0] += 1
573
+ update_display()
574
+ else:
575
+ # Normal mode: navigate sessions
576
+ if selected_idx[0] > 0:
577
+ selected_idx[0] -= 1
578
+ current_page[0] = ensure_visible_page(
579
+ selected_idx[0],
580
+ current_page[0],
581
+ len(entries),
582
+ PAGE_SIZE,
583
+ )
584
+ update_display()
585
+
586
+ @kb.add("down")
587
+ @kb.add("c-n") # Ctrl+N = next (Emacs-style)
588
+ def _(event):
589
+ if browse_mode[0]:
590
+ # In browse mode: go to newer message
591
+ if message_idx[0] > 0:
592
+ message_idx[0] -= 1
593
+ update_display()
594
+ else:
595
+ # Normal mode: navigate sessions
596
+ if selected_idx[0] < len(entries) - 1:
597
+ selected_idx[0] += 1
598
+ current_page[0] = ensure_visible_page(
599
+ selected_idx[0],
600
+ current_page[0],
601
+ len(entries),
602
+ PAGE_SIZE,
603
+ )
604
+ update_display()
605
+
606
+ @kb.add("left")
607
+ def _(event):
608
+ if current_page[0] > 0:
609
+ current_page[0] -= 1
610
+ selected_idx[0] = current_page[0] * PAGE_SIZE
611
+ update_display()
612
+
613
+ @kb.add("right")
614
+ def _(event):
615
+ if current_page[0] < total_pages - 1:
616
+ current_page[0] += 1
617
+ selected_idx[0] = current_page[0] * PAGE_SIZE
618
+ update_display()
619
+
620
+ @kb.add("e")
621
+ def _(event):
622
+ """Enter message browse mode."""
623
+ if browse_mode[0]:
624
+ return # Already in browse mode
625
+ entry = get_current_entry()
626
+ if entry:
627
+ session_name = entry[0]
628
+ try:
629
+ cached_history[0] = load_session(session_name, base_dir)
630
+ browse_mode[0] = True
631
+ message_idx[0] = 0 # Start at most recent
632
+ update_display()
633
+ except Exception:
634
+ pass # Silently fail if can't load
635
+
636
+ @kb.add("escape")
637
+ def _(event):
638
+ """Exit browse mode or cancel."""
639
+ if browse_mode[0]:
640
+ browse_mode[0] = False
641
+ cached_history[0] = None
642
+ message_idx[0] = 0
643
+ update_display()
644
+ else:
645
+ # Not in browse mode - treat as cancel
646
+ result[0] = None
647
+ event.app.exit()
648
+
649
+ @kb.add("q")
650
+ def _(event):
651
+ """Exit browse mode (only when in browse mode)."""
652
+ if browse_mode[0]:
653
+ browse_mode[0] = False
654
+ cached_history[0] = None
655
+ message_idx[0] = 0
656
+ update_display()
657
+
658
+ @kb.add("enter")
659
+ def _(event):
660
+ entry = get_current_entry()
661
+ if entry:
662
+ result[0] = entry[0] # Store session name
663
+ event.app.exit()
664
+
665
+ @kb.add("c-c")
666
+ def _(event):
667
+ result[0] = None
668
+ event.app.exit()
669
+
670
+ layout = Layout(root_container)
671
+ app = Application(
672
+ layout=layout,
673
+ key_bindings=kb,
674
+ full_screen=False,
675
+ mouse_support=False,
676
+ )
677
+
678
+ set_awaiting_user_input(True)
679
+
680
+ # Enter alternate screen buffer once for entire session
681
+ sys.stdout.write("\033[?1049h") # Enter alternate buffer
682
+ sys.stdout.write("\033[2J\033[H") # Clear and home
683
+ sys.stdout.flush()
684
+ await asyncio.sleep(0.05)
685
+
686
+ try:
687
+ # Initial display
688
+ update_display()
689
+
690
+ # Just clear the current buffer (don't switch buffers)
691
+ sys.stdout.write("\033[2J\033[H") # Clear screen within current buffer
692
+ sys.stdout.flush()
693
+
694
+ # Run application (stays in same alternate buffer)
695
+ await app.run_async()
696
+
697
+ finally:
698
+ # Exit alternate screen buffer once at end
699
+ sys.stdout.write("\033[?1049l") # Exit alternate buffer
700
+ sys.stdout.flush()
701
+ # Reset awaiting input flag
702
+ set_awaiting_user_input(False)
703
+
704
+ # Clear exit message
705
+ from code_muse.messaging import emit_info
706
+
707
+ emit_info("✓ Exited session browser")
708
+
709
+ return result[0]