utim-cli 2.5.5__tar.gz → 2.6.0__tar.gz

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 (226) hide show
  1. {utim_cli-2.5.5 → utim_cli-2.6.0}/CHANGELOG.md +9 -0
  2. {utim_cli-2.5.5/utim_cli.egg-info → utim_cli-2.6.0}/PKG-INFO +1 -1
  3. {utim_cli-2.5.5 → utim_cli-2.6.0}/pyproject.toml +1 -1
  4. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim/__init__.py +1 -1
  5. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/__init__.py +1 -1
  6. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_version.py +1 -1
  7. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/agent.py +40 -32
  8. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/client_utils.py +148 -2
  9. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/config.py +229 -0
  10. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/context_pruner.py +544 -71
  11. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/crawler/markdown_distiller.py +72 -2
  12. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/mcp_client.py +50 -8
  13. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator.py +67 -1
  14. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator_llm_transport.py +531 -157
  15. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator_tool_exec.py +348 -35
  16. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/__init__.py +1 -1
  17. utim_cli-2.6.0/utim_cli/sdk/config.py +95 -0
  18. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/sidecar.py +171 -163
  19. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/api_models_db.py +125 -4
  20. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/auth.py +11 -1
  21. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/db.py +34 -11
  22. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/email_utils.py +1 -1
  23. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/models.py +26 -1
  24. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/router.py +66 -35
  25. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/admin_separated_models_routes.py +1551 -1254
  26. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/api_gateway_extensions_routes.py +0 -2
  27. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/api_models_admin_routes.py +230 -33
  28. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/api_models_public_routes.py +353 -127
  29. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/billing_routes.py +78 -3
  30. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/completion_routes.py +110 -7
  31. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/credit_routes.py +6 -1
  32. utim_cli-2.6.0/utim_cli/server/routes/referral_routes.py +554 -0
  33. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/wallet_routes.py +329 -193
  34. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/utils.py +186 -0
  35. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/share_tui.py +4 -3
  36. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tools.py +218 -11
  37. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/model_dialog.py +25 -7
  38. utim_cli-2.6.0/utim_cli/tui/personality_dialog.py +642 -0
  39. utim_cli-2.6.0/utim_cli/tui/spinner.py +379 -0
  40. utim_cli-2.6.0/utim_cli/tui/stream_diagnostics.py +243 -0
  41. utim_cli-2.6.0/utim_cli/tui/thinking_phrases.py +375 -0
  42. utim_cli-2.6.0/utim_cli/tui/token_meter.py +612 -0
  43. utim_cli-2.6.0/utim_cli/tui/tool_theme_preview.py +237 -0
  44. utim_cli-2.6.0/utim_cli/tui/tool_themes.py +1182 -0
  45. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/utim.py +780 -60
  46. {utim_cli-2.5.5 → utim_cli-2.6.0/utim_cli.egg-info}/PKG-INFO +1 -1
  47. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli.egg-info/SOURCES.txt +6 -0
  48. utim_cli-2.5.5/utim_cli/sdk/config.py +0 -41
  49. utim_cli-2.5.5/utim_cli/server/routes/referral_routes.py +0 -308
  50. utim_cli-2.5.5/utim_cli/tui/personality_dialog.py +0 -192
  51. {utim_cli-2.5.5 → utim_cli-2.6.0}/LICENSE +0 -0
  52. {utim_cli-2.5.5 → utim_cli-2.6.0}/MANIFEST.in +0 -0
  53. {utim_cli-2.5.5 → utim_cli-2.6.0}/README.md +0 -0
  54. {utim_cli-2.5.5 → utim_cli-2.6.0}/setup.cfg +0 -0
  55. {utim_cli-2.5.5 → utim_cli-2.6.0}/setup.py +0 -0
  56. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_chip_test.py +0 -0
  57. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_verify_repro.py +0 -0
  58. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_verify_syntax.py +0 -0
  59. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_wizard_smoke_test.py +0 -0
  60. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/adaptation_engine.py +0 -0
  61. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/architecture_indexer.py +0 -0
  62. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/ask_helper.py +0 -0
  63. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/auth.py +0 -0
  64. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/background_models.py +0 -0
  65. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/backup.py +0 -0
  66. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/behavior_telemetry.py +0 -0
  67. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/billing.py +0 -0
  68. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/blender_agent.py +0 -0
  69. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/bootstrap.py +0 -0
  70. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/brain.py +0 -0
  71. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
  72. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/constants.py +0 -0
  73. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/context_inspector.py +0 -0
  74. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/crawler/__init__.py +0 -0
  75. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/crawler/engine.py +0 -0
  76. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/dependency_manager.py +0 -0
  77. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/doctor.py +0 -0
  78. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/evidence_registry.py +0 -0
  79. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/harbor.py +0 -0
  80. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/knowledge_graph.py +0 -0
  81. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/local_db.py +0 -0
  82. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/logger.py +0 -0
  83. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/mcp_clean_wrapper.py +0 -0
  84. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/mcp_registry.json +0 -0
  85. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/models.txt +0 -0
  86. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator_helpers.py +0 -0
  87. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator_turn_history.py +0 -0
  88. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/personalities.py +0 -0
  89. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/platform_installer.py +0 -0
  90. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/__init__.py +0 -0
  91. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/hooks.py +0 -0
  92. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/loader.py +0 -0
  93. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/manager.py +0 -0
  94. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/registry.py +0 -0
  95. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/runtime.py +0 -0
  96. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/schema.py +0 -0
  97. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/tool_loader.py +0 -0
  98. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/reflection.py +0 -0
  99. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/report.py +0 -0
  100. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/scrapy_search.py +0 -0
  101. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/context.py +0 -0
  102. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/events.py +0 -0
  103. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/handlers.py +0 -0
  104. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/session.py +0 -0
  105. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/__init__.py +0 -0
  106. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/admin_auth.py +0 -0
  107. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/api_request_logs.py +0 -0
  108. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/attribution.py +0 -0
  109. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/audit_log.py +0 -0
  110. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/batch_processor.py +0 -0
  111. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/bot_detection.py +0 -0
  112. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/captcha.py +0 -0
  113. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/cli_auth.py +0 -0
  114. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/concurrency.py +0 -0
  115. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/crypto_utils.py +0 -0
  116. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/disposable_email_guard.py +0 -0
  117. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  118. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/about.md +0 -0
  119. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/changelog.md +0 -0
  120. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/docs.md +0 -0
  121. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/features.md +0 -0
  122. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/license.md +0 -0
  123. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/pricing.md +0 -0
  124. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/privacy.md +0 -0
  125. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/refund.md +0 -0
  126. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/support.md +0 -0
  127. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/terms.md +0 -0
  128. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/exchange_rate.py +0 -0
  129. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/firebase.py +0 -0
  130. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/firewall.py +0 -0
  131. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/fix_duplicate_users.py +0 -0
  132. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/history.py +0 -0
  133. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/hobby_throttler.py +0 -0
  134. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/key_rotator.py +0 -0
  135. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/local_llm.py +0 -0
  136. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/logging_config.py +0 -0
  137. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/maintenance.py +0 -0
  138. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/model_agent.py +0 -0
  139. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/plan_integrity.py +0 -0
  140. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/platform_billing.py +0 -0
  141. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/pricing_updater.py +0 -0
  142. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provider_balance.py +0 -0
  143. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provider_batcher.py +0 -0
  144. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provider_discovery.py +0 -0
  145. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provider_router.py +0 -0
  146. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provision_build.py +0 -0
  147. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/purge_disposable_users.py +0 -0
  148. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/rate_limit.py +0 -0
  149. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/request_batcher.py +0 -0
  150. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/request_queue.py +0 -0
  151. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/rewards_engine.py +0 -0
  152. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/__init__.py +0 -0
  153. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/activity_routes.py +0 -0
  154. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
  155. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/admin_db_routes.py +0 -0
  156. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/admin_providers_routes.py +0 -0
  157. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/api_key_routes.py +0 -0
  158. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/auth_routes.py +0 -0
  159. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/compression_routes.py +0 -0
  160. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/discord_routes.py +0 -0
  161. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/feedback_routes.py +0 -0
  162. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/marketplace_routes.py +0 -0
  163. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/provider_compat_routes.py +0 -0
  164. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/quota_routes.py +0 -0
  165. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/quota_share_routes.py +0 -0
  166. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/rewards_routes.py +0 -0
  167. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/security_routes.py +0 -0
  168. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/session_routes.py +0 -0
  169. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/share_routes.py +0 -0
  170. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/webhook_routes.py +0 -0
  171. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/security_alerts.py +0 -0
  172. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/server.py +0 -0
  173. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/signup_guard.py +0 -0
  174. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/storage_nodes.py +0 -0
  175. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/tier_tables.py +0 -0
  176. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/uptime_monitor.py +0 -0
  177. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/share.py +0 -0
  178. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/situational_scoring.py +0 -0
  179. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/state.py +0 -0
  180. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/subagent_manager.py +0 -0
  181. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/syntax_fix.py +0 -0
  182. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/task_dispatcher.py +0 -0
  183. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/task_logger.py +0 -0
  184. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tools_grep.py +0 -0
  185. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/__init__.py +0 -0
  186. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/compression_dialog.py +0 -0
  187. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/feedback_dialog.py +0 -0
  188. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/history_dialog.py +0 -0
  189. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/marketplace_app_state.py +0 -0
  190. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/marketplace_dialog.py +0 -0
  191. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  192. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/mcp_dialog.py +0 -0
  193. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/miniagents_dialog.py +0 -0
  194. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/plugins_dialog.py +0 -0
  195. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/publish_dialog.py +0 -0
  196. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/quota_dialog.py +0 -0
  197. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  198. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/quota_share_dialog.py +0 -0
  199. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/resume_dialog.py +0 -0
  200. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/rewards_tui.py +0 -0
  201. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/skills_dialog.py +0 -0
  202. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/subagents_dialog.py +0 -0
  203. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/tasks_dialog.py +0 -0
  204. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/thinking_display.py +0 -0
  205. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/tools_dialog.py +0 -0
  206. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/update_dialog.py +0 -0
  207. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/verbosity_dialog.py +0 -0
  208. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/update_checker.py +0 -0
  209. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/utilities.py +0 -0
  210. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/utimmodel.txt +0 -0
  211. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/vector_memory.py +0 -0
  212. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice.py +0 -0
  213. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_coding.py +0 -0
  214. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_events.py +0 -0
  215. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_handler.py +0 -0
  216. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_instant.py +0 -0
  217. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_orchestrator.py +0 -0
  218. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_session.py +0 -0
  219. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_stt.py +0 -0
  220. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_tts.py +0 -0
  221. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/wheel.py +0 -0
  222. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/workspace.py +0 -0
  223. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli.egg-info/dependency_links.txt +0 -0
  224. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli.egg-info/entry_points.txt +0 -0
  225. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli.egg-info/requires.txt +0 -0
  226. {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.6.0] - 2026-11-03
4
+
5
+ ### ✨ New Theme
6
+ - **Fresh UI Theme**: A brand-new visual theme for the CLI and website, with refreshed colours, cleaner spacing, and a sharper overall look.
7
+ - **UI Enhancements**: Polish across menus, panels, and prompts for a more consistent and modern feel.
8
+
9
+ ### 🐛 Fixes
10
+ - **Bug Fixes**: A round of stability and reliability fixes across the app.
11
+
3
12
  ## [2.5.5] - 2026-10-29
4
13
 
5
14
  ### 🐛 Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.5.5
3
+ Version: 2.6.0
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: Emend AI Proprietary EULA
6
6
  Project-URL: Homepage, https://utim.dev
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.5.5"
7
+ version = "2.6.0"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -14,7 +14,7 @@ Quickstart:
14
14
  try:
15
15
  from utim_cli._version import VERSION as __version__
16
16
  except ImportError:
17
- __version__ = "2.5.5"
17
+ __version__ = "2.6.0"
18
18
 
19
19
  # Developer-friendly core classes (similar to `import genai` / `import openai`)
20
20
  from utim_cli.sdk.session import UtimSession as Session
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "2.5.5"
8
+ __version__ = "2.6.0"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.5.5"
14
+ VERSION = "2.6.0"
15
15
 
16
16
 
@@ -228,6 +228,42 @@ class ReActAgent:
228
228
  self.console.print(_pre)
229
229
  pre_plain = f"{meta['verb']}({primary_arg}) (ctrl+o to expand)" if primary_arg else f"{meta['verb']} (ctrl+o to expand)"
230
230
  self._last_running_indicator_len = len(pre_plain)
231
+ # Remember that a provisional line is on screen, and the paint
232
+ # generation at the moment we wrote it. _erase_running_line() uses
233
+ # both: a blind CSI-1F erase is only safe while nothing has painted
234
+ # since, otherwise it corrupts the screen or leaves a duplicate header
235
+ # (the "Verb(x) · running" + "Verb(x)" double print).
236
+ self._running_line_live = True
237
+ try:
238
+ from utim_cli.state import STATE as _STG
239
+ self._running_line_paint_gen = _STG.get("_paint_gen", 0)
240
+ except Exception:
241
+ self._running_line_paint_gen = None
242
+
243
+ def _erase_running_line() -> None:
244
+ """Guarded replacement for the blind CSI-1F erase loops below."""
245
+ if not getattr(self, "_running_line_live", False):
246
+ return
247
+ self._running_line_live = False
248
+ try:
249
+ from utim_cli.state import STATE as _ST
250
+ if _ST.get("_paint_gen", 0) != getattr(self, "_running_line_paint_gen", None):
251
+ # Something repainted while the tool ran; the cursor is no
252
+ # longer under our header. Do not blind-erase.
253
+ return
254
+ except Exception:
255
+ pass
256
+ try:
257
+ width = self.console.size.width
258
+ except Exception:
259
+ width = 80
260
+ import math
261
+ num_lines = math.ceil(
262
+ getattr(self, "_last_running_indicator_len", 0) / max(1, width)
263
+ )
264
+ num_lines = max(0, min(num_lines, 5)) # never wipe more than 5 rows
265
+ for _ in range(num_lines):
266
+ self.console.print("\033[F\033[K", end="")
231
267
 
232
268
  if "__" in tool_name:
233
269
  server_name, actual_tool_name = tool_name.split("__", 1)
@@ -240,25 +276,11 @@ class ReActAgent:
240
276
  # Temporarily register metadata for render
241
277
  TOOL_META[tool_name] = {"icon": "", "verb": f"Calling {server_name} ➔ {actual_tool_name}", "color": "#cba6f7"}
242
278
  # Erase the running line first
243
- try:
244
- width = self.console.size.width
245
- except Exception:
246
- width = 80
247
- import math
248
- num_lines = math.ceil(getattr(self, "_last_running_indicator_len", 40) / max(1, width))
249
- for _ in range(num_lines):
250
- self.console.print("\033[F\033[K", end="")
279
+ _erase_running_line()
251
280
  self._render_tool_panel(tool_name, arguments, result)
252
281
  return str(result)
253
282
  except Exception as e:
254
- try:
255
- width = self.console.size.width
256
- except Exception:
257
- width = 80
258
- import math
259
- num_lines = math.ceil(getattr(self, "_last_running_indicator_len", 40) / max(1, width))
260
- for _ in range(num_lines):
261
- self.console.print("\033[F\033[K", end="")
283
+ _erase_running_line()
262
284
  return f"Error executing MCP tool {tool_name}: {str(e)}"
263
285
 
264
286
  utim_tools, tool_functions = get_tools()
@@ -275,25 +297,11 @@ class ReActAgent:
275
297
  filtered_args = dict(arguments) if has_var_kw else {k: v for k, v in arguments.items() if k in sig.parameters}
276
298
  result = tool_functions[tool_name](**filtered_args)
277
299
  # Erase the running line first
278
- try:
279
- width = self.console.size.width
280
- except Exception:
281
- width = 80
282
- import math
283
- num_lines = math.ceil(getattr(self, "_last_running_indicator_len", 40) / max(1, width))
284
- for _ in range(num_lines):
285
- self.console.print("\033[F\033[K", end="")
300
+ _erase_running_line()
286
301
  self._render_tool_panel(tool_name, arguments, result)
287
302
  return str(result)
288
303
  except Exception as e:
289
- try:
290
- width = self.console.size.width
291
- except Exception:
292
- width = 80
293
- import math
294
- num_lines = math.ceil(getattr(self, "_last_running_indicator_len", 40) / max(1, width))
295
- for _ in range(num_lines):
296
- self.console.print("\033[F\033[K", end="")
304
+ _erase_running_line()
297
305
 
298
306
  # Auto-repair: if the tool failed due to a syntax error in the
299
307
  # generated code, attempt an "on the go" syntax fix before
@@ -6,6 +6,8 @@ import hashlib
6
6
  import uuid
7
7
  import math
8
8
  import threading
9
+ import time
10
+ import time
9
11
  from typing import Optional, List, Dict, Any, Union, Tuple
10
12
  import requests
11
13
  from utim_cli.config import config
@@ -1928,8 +1930,7 @@ def get_session_usage(session_id: str = "") -> dict:
1928
1930
  return _read_all_usage().get(eff_id, empty)
1929
1931
 
1930
1932
 
1931
- def update_session_usage(
1932
- usage_type: str,
1933
+ def update_session_usage( usage_type: str,
1933
1934
  input_tokens: int = 0,
1934
1935
  output_tokens: int = 0,
1935
1936
  credits_used: float = 0.0,
@@ -1973,9 +1974,154 @@ def update_session_usage(
1973
1974
  entry["tools_credits"] = entry.get("tools_credits", 0.0) + credits_used
1974
1975
 
1975
1976
  all_sessions[eff_id] = entry
1977
+ # Stamp the write time so `get_monthly_usage` can bucket this session into
1978
+ # the correct calendar month later (sessions without a stamp are skipped).
1979
+ try:
1980
+ entry["updated_at"] = time.time()
1981
+ except Exception:
1982
+ pass
1976
1983
  _write_all_usage(all_sessions)
1977
1984
 
1978
1985
 
1986
+ def _sum_sessions(all_sessions, include_sid=None, month_filter=None):
1987
+ """Shared aggregation core for monthly / lifetime usage rollups.
1988
+
1989
+ Parameters
1990
+ ----------
1991
+ include_sid : session id that is *always* counted regardless of date
1992
+ (the live session is by definition current).
1993
+ month_filter : (year, month) tuple, or None to count every session.
1994
+
1995
+ Returns
1996
+ -------
1997
+ dict with input_tokens, output_tokens, total_tokens, credits,
1998
+ sessions_counted, sessions_skipped.
1999
+
2000
+ Sessions lacking any timestamp are counted only when `month_filter` is None
2001
+ (lifetime), because there is no month to mis-attribute them to.
2002
+ """
2003
+ import datetime as _dt
2004
+
2005
+ out = {
2006
+ "input_tokens": 0,
2007
+ "output_tokens": 0,
2008
+ "total_tokens": 0,
2009
+ "credits": 0.0,
2010
+ "sessions_counted": 0,
2011
+ "sessions_skipped": 0,
2012
+ }
2013
+
2014
+ for sid, entry in (all_sessions or {}).items():
2015
+ if not isinstance(entry, dict):
2016
+ continue
2017
+
2018
+ in_tok = int(entry.get("main_agent_input_tokens", 0) or 0) + int(
2019
+ entry.get("tools_input_tokens", 0) or 0
2020
+ )
2021
+ out_tok = int(entry.get("main_agent_output_tokens", 0) or 0) + int(
2022
+ entry.get("tools_output_tokens", 0) or 0
2023
+ )
2024
+ cred = float(entry.get("main_agent_credits", 0.0) or 0.0) + float(
2025
+ entry.get("tools_credits", 0.0) or 0.0
2026
+ )
2027
+
2028
+ if month_filter is None:
2029
+ # Lifetime: no attribution problem, everything counts.
2030
+ counts = True
2031
+ elif include_sid is not None and sid == include_sid:
2032
+ counts = True
2033
+ else:
2034
+ ts = entry.get("updated_at") or entry.get("created_at")
2035
+ if not ts:
2036
+ counts = False
2037
+ else:
2038
+ try:
2039
+ stamp = _dt.datetime.fromtimestamp(float(ts))
2040
+ counts = (stamp.year, stamp.month) == month_filter
2041
+ except (TypeError, ValueError, OSError):
2042
+ counts = False
2043
+
2044
+ if not counts:
2045
+ out["sessions_skipped"] += 1
2046
+ continue
2047
+
2048
+ out["input_tokens"] += in_tok
2049
+ out["output_tokens"] += out_tok
2050
+ out["credits"] += cred
2051
+ out["sessions_counted"] += 1
2052
+
2053
+ out["total_tokens"] = out["input_tokens"] + out["output_tokens"]
2054
+ return out
2055
+
2056
+
2057
+ def get_total_usage() -> dict:
2058
+ """Lifetime token usage across every session on disk."""
2059
+ try:
2060
+ return _sum_sessions(_read_all_usage(), include_sid=None, month_filter=None)
2061
+ except Exception:
2062
+ return {
2063
+ "input_tokens": 0,
2064
+ "output_tokens": 0,
2065
+ "total_tokens": 0,
2066
+ "credits": 0.0,
2067
+ "sessions_counted": 0,
2068
+ "sessions_skipped": 0,
2069
+ }
2070
+
2071
+
2072
+ def get_monthly_usage(year: int = 0, month: int = 0, now: float = None) -> dict:
2073
+ """Aggregate token usage across every session that falls in a calendar month.
2074
+
2075
+ Session entries do not carry their own timestamps (the on-disk format is a
2076
+ flat token tally per session id), so attributable data is limited. Two
2077
+ sources are combined:
2078
+
2079
+ 1. The *current* session, which is always attributable to the current
2080
+ month and is counted in full.
2081
+ 2. Any per-session `updated_at` epoch stamp, when present, which lets
2082
+ older sessions be bucketed by their actual month.
2083
+
2084
+ Sessions with no timestamp are deliberately **excluded** rather than
2085
+ guessed at, so the number never silently inflates.
2086
+
2087
+ Parameters
2088
+ ----------
2089
+ year, month : target month (defaults to the current local month)
2090
+ now : injectable clock (epoch seconds) for tests
2091
+
2092
+ Returns
2093
+ -------
2094
+ dict with keys:
2095
+ input_tokens, output_tokens, total_tokens, credits,
2096
+ sessions_counted, sessions_skipped, year, month
2097
+ """
2098
+ import datetime as _dt
2099
+
2100
+ ref = _dt.datetime.fromtimestamp(now if now is not None else time.time())
2101
+ tgt_year = year or ref.year
2102
+ tgt_month = month or ref.month
2103
+
2104
+ try:
2105
+ result = _sum_sessions(
2106
+ _read_all_usage(),
2107
+ include_sid=_resolve_session_id(""),
2108
+ month_filter=(tgt_year, tgt_month),
2109
+ )
2110
+ except Exception:
2111
+ result = {
2112
+ "input_tokens": 0,
2113
+ "output_tokens": 0,
2114
+ "total_tokens": 0,
2115
+ "credits": 0.0,
2116
+ "sessions_counted": 0,
2117
+ "sessions_skipped": 0,
2118
+ }
2119
+
2120
+ result["year"] = tgt_year
2121
+ result["month"] = tgt_month
2122
+ return result
2123
+
2124
+
1979
2125
  def request_server_compression(
1980
2126
  messages,
1981
2127
  max_tokens: int = 1500,
@@ -739,3 +739,232 @@ class Config:
739
739
  config = Config()
740
740
  # Convenience constant for direct access
741
741
  BLENDER_PATH = config.blender_path
742
+
743
+
744
+ # ── Tool-call theme preference ────────────────────────────────────────────────
745
+ # Stored under the ``ui`` key as ``{"tool_theme": "<name>"}`` so future UI
746
+ # preferences can share the namespace without colliding with agent settings.
747
+ _TOOL_THEME_KEY = "ui"
748
+ _TOOL_THEME_FIELD = "tool_theme"
749
+
750
+
751
+ def get_tool_theme() -> str:
752
+ """Return the user's saved tool-call theme name (defaults to 'default')."""
753
+ try:
754
+ ui = config.get(_TOOL_THEME_KEY, {}) or {}
755
+ name = ui.get(_TOOL_THEME_FIELD)
756
+ if isinstance(name, str) and name.strip():
757
+ return name.strip().lower()
758
+ except Exception as e:
759
+ logger.warning(f"Could not read tool theme preference: {e}")
760
+ return "default"
761
+
762
+
763
+ def set_tool_theme(name: str) -> bool:
764
+ """Persist the tool-call theme name. Returns True on success."""
765
+ try:
766
+ ui = dict(config.get(_TOOL_THEME_KEY, {}) or {})
767
+ ui[_TOOL_THEME_FIELD] = str(name).strip().lower()
768
+ config.set(_TOOL_THEME_KEY, ui)
769
+ return True
770
+ except Exception as e:
771
+ logger.error(f"Could not save tool theme preference: {e}")
772
+ return False
773
+
774
+
775
+ # ── Two-axis theme composition (palette × design) ─────────────────────────────
776
+ # A single `tool_theme` name was a dead end: users had to pick a bundle that
777
+ # coupled a colour scheme to a layout, so "I want the box design but in Nord"
778
+ # was impossible without a fresh theme instance for every combination.
779
+ #
780
+ # The composed look is now stored as TWO independent preferences:
781
+ # palette : which colour scheme (default / nord / gruvbox / dracula / ...)
782
+ # design : which structural layout (tree / box / rule / bar / bracket / ...)
783
+ #
784
+ # Resolution order for a theme *name*:
785
+ # 1. explicit composed palette/design pair, if either is set
786
+ # 2. the legacy single `tool_theme` name (pre-composition configs)
787
+ # 3. "default"
788
+ # The legacy field is never deleted, so downgrading loses nothing.
789
+ _PALETTE_FIELD = "tool_palette"
790
+ _DESIGN_FIELD = "tool_design"
791
+
792
+
793
+ def _ui_get(field_name: str) -> str:
794
+ try:
795
+ ui = config.get(_TOOL_THEME_KEY, {}) or {}
796
+ val = ui.get(field_name)
797
+ if isinstance(val, str) and val.strip():
798
+ return val.strip().lower()
799
+ except Exception as e:
800
+ logger.warning(f"Could not read ui.{field_name}: {e}")
801
+ return ""
802
+
803
+
804
+ def _ui_set(**fields) -> bool:
805
+ try:
806
+ ui = dict(config.get(_TOOL_THEME_KEY, {}) or {})
807
+ for k, v in fields.items():
808
+ if v is None:
809
+ ui.pop(k, None)
810
+ else:
811
+ ui[k] = str(v).strip().lower()
812
+ config.set(_TOOL_THEME_KEY, ui)
813
+ return True
814
+ except Exception as e:
815
+ logger.error(f"Could not save UI preference: {e}")
816
+ return False
817
+
818
+
819
+ def get_tool_palette() -> str:
820
+ """Colour-scheme half of the composed theme ('' when unset)."""
821
+ return _ui_get(_PALETTE_FIELD)
822
+
823
+
824
+ def get_tool_design() -> str:
825
+ """Layout/CAD half of the composed theme ('' when unset)."""
826
+ return _ui_get(_DESIGN_FIELD)
827
+
828
+
829
+ def set_tool_palette(name: str) -> bool:
830
+ """Persist the palette half only — leaves the design untouched."""
831
+ return _ui_set(**{_PALETTE_FIELD: name})
832
+
833
+
834
+ def set_tool_design(name: str) -> bool:
835
+ """Persist the design half only — leaves the palette untouched."""
836
+ return _ui_set(**{_DESIGN_FIELD: name})
837
+
838
+
839
+ def set_composed_theme(palette: str = None, design: str = None) -> bool:
840
+ """Persist palette and/or design in a single write.
841
+
842
+ ``None`` means "leave as-is" rather than "clear" — the dialog writes both
843
+ halves together so a half-configured state never lands on disk.
844
+ """
845
+ fields = {}
846
+ if palette is not None:
847
+ fields[_PALETTE_FIELD] = palette
848
+ if design is not None:
849
+ fields[_DESIGN_FIELD] = design
850
+ # Keep the legacy field pointing at the composed name for older code paths
851
+ # and for a readable config file.
852
+ if palette or design:
853
+ try:
854
+ from utim_cli.tui import tool_themes as _tt
855
+
856
+ fields[_TOOL_THEME_FIELD] = _tt.compose_name(
857
+ palette or get_tool_palette(), design or get_tool_design()
858
+ )
859
+ except Exception:
860
+ pass
861
+ return _ui_set(**fields)
862
+
863
+
864
+ def clear_composed_theme() -> bool:
865
+ """Forget both halves, falling back to the legacy / default theme."""
866
+ return _ui_set(**{_PALETTE_FIELD: None, _DESIGN_FIELD: None})
867
+
868
+
869
+ # ── Two-axis appearance: palette × design ────────────────────────────────────
870
+ #
871
+ # A palette answers "what colours?", a design answers "what shape?". Keeping
872
+ # them independent means N palettes × M designs = N*M looks, instead of N*M
873
+ # hand-written themes. Both are stored next to ``tool_theme`` in the same ``ui``
874
+ # namespace so an older UTIM that only understands ``tool_theme`` still reads a
875
+ # valid name and degrades gracefully.
876
+ #
877
+ # ``tool_theme`` keeps its legacy meaning: the *fully resolved* combination.
878
+ # When the user equips a palette in the picker we write both keys, so config
879
+ # stays self-describing for anyone inspecting it by hand.
880
+
881
+ _TOOL_PALETTE_FIELD = "tool_palette"
882
+ _TOOL_DESIGN_FIELD = "tool_design"
883
+
884
+
885
+ def get_tool_palette() -> str:
886
+ """Return the saved palette name, or '' if the user never picked one.
887
+
888
+ '' means "derive it from the legacy ``tool_theme`` value", which is how a
889
+ user who only ever ran ``/theme nord`` ends up with palette=nord and the
890
+ current design intact.
891
+ """
892
+ try:
893
+ ui = config.get(_TOOL_THEME_KEY, {}) or {}
894
+ name = ui.get(_TOOL_PALETTE_FIELD)
895
+ if isinstance(name, str) and name.strip():
896
+ return name.strip().lower()
897
+ except Exception as e:
898
+ logger.warning(f"Could not read tool palette preference: {e}")
899
+ return ""
900
+
901
+
902
+ def set_tool_palette(name: str) -> bool:
903
+ """Persist the palette choice (client-side only, cheap to write)."""
904
+ try:
905
+ ui = dict(config.get(_TOOL_THEME_KEY, {}) or {})
906
+ ui[_TOOL_PALETTE_FIELD] = str(name).strip().lower()
907
+ config.set(_TOOL_THEME_KEY, ui)
908
+ return True
909
+ except Exception as e:
910
+ logger.error(f"Could not save tool palette preference: {e}")
911
+ return False
912
+
913
+
914
+ def get_tool_design() -> str:
915
+ """Return the saved design (layout) name, or '' if never picked."""
916
+ try:
917
+ ui = config.get(_TOOL_THEME_KEY, {}) or {}
918
+ name = ui.get(_TOOL_DESIGN_FIELD)
919
+ if isinstance(name, str) and name.strip():
920
+ return name.strip().lower()
921
+ except Exception as e:
922
+ logger.warning(f"Could not read tool design preference: {e}")
923
+ return ""
924
+
925
+
926
+ def set_tool_design(name: str) -> bool:
927
+ """Persist the design (layout) choice."""
928
+ try:
929
+ ui = dict(config.get(_TOOL_THEME_KEY, {}) or {})
930
+ ui[_TOOL_DESIGN_FIELD] = str(name).strip().lower()
931
+ config.set(_TOOL_THEME_KEY, ui)
932
+ return True
933
+ except Exception as e:
934
+ logger.error(f"Could not save tool design preference: {e}")
935
+ return False
936
+
937
+
938
+ def get_tool_appearance() -> dict:
939
+ """One read for the resolver: {'palette', 'design', 'legacy', 'theme'}."""
940
+ return {
941
+ "palette": get_tool_palette(),
942
+ "design": get_tool_design(),
943
+ "legacy": get_tool_theme(),
944
+ }
945
+
946
+
947
+ def set_tool_appearance(palette: str | None = None,
948
+ design: str | None = None) -> bool:
949
+ """Persist either or both axes in a single write (one disk hit).
950
+
951
+ ``tool_theme`` is rewritten to the resolved combination so the config file
952
+ remains self-describing and older readers keep working.
953
+ """
954
+ try:
955
+ ui = dict(config.get(_TOOL_THEME_KEY, {}) or {})
956
+ if palette is not None:
957
+ ui[_TOOL_PALETTE_FIELD] = str(palette).strip().lower()
958
+ if design is not None:
959
+ ui[_TOOL_DESIGN_FIELD] = str(design).strip().lower()
960
+ p = ui.get(_TOOL_PALETTE_FIELD) or ""
961
+ d = ui.get(_TOOL_DESIGN_FIELD) or ""
962
+ if p and d and d != "tree":
963
+ ui[_TOOL_THEME_FIELD] = f"{p}+{d}"
964
+ elif p:
965
+ ui[_TOOL_THEME_FIELD] = p
966
+ config.set(_TOOL_THEME_KEY, ui)
967
+ return True
968
+ except Exception as e:
969
+ logger.error(f"Could not save tool appearance: {e}")
970
+ return False