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.
- {utim_cli-2.5.5 → utim_cli-2.6.0}/CHANGELOG.md +9 -0
- {utim_cli-2.5.5/utim_cli.egg-info → utim_cli-2.6.0}/PKG-INFO +1 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/pyproject.toml +1 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim/__init__.py +1 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/__init__.py +1 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_version.py +1 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/agent.py +40 -32
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/client_utils.py +148 -2
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/config.py +229 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/context_pruner.py +544 -71
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/crawler/markdown_distiller.py +72 -2
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/mcp_client.py +50 -8
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator.py +67 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator_llm_transport.py +531 -157
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator_tool_exec.py +348 -35
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/__init__.py +1 -1
- utim_cli-2.6.0/utim_cli/sdk/config.py +95 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/sidecar.py +171 -163
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/api_models_db.py +125 -4
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/auth.py +11 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/db.py +34 -11
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/email_utils.py +1 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/models.py +26 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/router.py +66 -35
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/admin_separated_models_routes.py +1551 -1254
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/api_gateway_extensions_routes.py +0 -2
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/api_models_admin_routes.py +230 -33
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/api_models_public_routes.py +353 -127
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/billing_routes.py +78 -3
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/completion_routes.py +110 -7
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/credit_routes.py +6 -1
- utim_cli-2.6.0/utim_cli/server/routes/referral_routes.py +554 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/wallet_routes.py +329 -193
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/utils.py +186 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/share_tui.py +4 -3
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tools.py +218 -11
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/model_dialog.py +25 -7
- utim_cli-2.6.0/utim_cli/tui/personality_dialog.py +642 -0
- utim_cli-2.6.0/utim_cli/tui/spinner.py +379 -0
- utim_cli-2.6.0/utim_cli/tui/stream_diagnostics.py +243 -0
- utim_cli-2.6.0/utim_cli/tui/thinking_phrases.py +375 -0
- utim_cli-2.6.0/utim_cli/tui/token_meter.py +612 -0
- utim_cli-2.6.0/utim_cli/tui/tool_theme_preview.py +237 -0
- utim_cli-2.6.0/utim_cli/tui/tool_themes.py +1182 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/utim.py +780 -60
- {utim_cli-2.5.5 → utim_cli-2.6.0/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli.egg-info/SOURCES.txt +6 -0
- utim_cli-2.5.5/utim_cli/sdk/config.py +0 -41
- utim_cli-2.5.5/utim_cli/server/routes/referral_routes.py +0 -308
- utim_cli-2.5.5/utim_cli/tui/personality_dialog.py +0 -192
- {utim_cli-2.5.5 → utim_cli-2.6.0}/LICENSE +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/MANIFEST.in +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/README.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/setup.cfg +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/setup.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_chip_test.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_verify_repro.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_verify_syntax.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/_wizard_smoke_test.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/adaptation_engine.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/architecture_indexer.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/auth.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/background_models.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/backup.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/behavior_telemetry.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/billing.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/brain.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/builtin/skills/utim-guide/SKILL.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/constants.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/context_inspector.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/crawler/__init__.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/crawler/engine.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/dependency_manager.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/doctor.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/evidence_registry.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/harbor.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/local_db.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/logger.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/models.txt +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator_helpers.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/orchestrator_turn_history.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/personalities.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/platform_installer.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/hooks.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/loader.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/manager.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/runtime.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/plugins/tool_loader.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/reflection.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/report.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/api_request_logs.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/bot_detection.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/concurrency.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/crypto_utils.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/disposable_email_guard.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/firewall.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/history.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/hobby_throttler.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/key_rotator.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/maintenance.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/plan_integrity.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/platform_billing.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provider_balance.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provider_batcher.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provider_discovery.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provider_router.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/purge_disposable_users.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/request_batcher.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/request_queue.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/activity_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/admin_bonus_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/admin_db_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/admin_providers_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/api_key_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/compression_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/discord_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/provider_compat_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/routes/webhook_routes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/security_alerts.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/server.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/signup_guard.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/tier_tables.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/server/uptime_monitor.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/share.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/state.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/syntax_fix.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/task_logger.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tools_grep.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/compression_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/tui/verbosity_dialog.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/update_checker.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/utilities.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_coding.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_events.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_handler.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_instant.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_orchestrator.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_session.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_stt.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/voice_tts.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/wheel.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli/workspace.py +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.5.5 → utim_cli-2.6.0}/utim_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
@@ -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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|