utim-cli 2.0.1__tar.gz → 2.0.2__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 (107) hide show
  1. {utim_cli-2.0.1 → utim_cli-2.0.2}/CHANGELOG.md +5 -0
  2. {utim_cli-2.0.1/utim_cli.egg-info → utim_cli-2.0.2}/PKG-INFO +1 -1
  3. {utim_cli-2.0.1 → utim_cli-2.0.2}/pyproject.toml +1 -1
  4. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/__init__.py +1 -1
  5. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_version.py +1 -1
  6. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/orchestrator.py +44 -187
  7. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tools.py +170 -28
  8. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/model_dialog.py +40 -36
  9. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/utim.py +3 -3
  10. {utim_cli-2.0.1 → utim_cli-2.0.2/utim_cli.egg-info}/PKG-INFO +1 -1
  11. {utim_cli-2.0.1 → utim_cli-2.0.2}/LICENSE +0 -0
  12. {utim_cli-2.0.1 → utim_cli-2.0.2}/MANIFEST.in +0 -0
  13. {utim_cli-2.0.1 → utim_cli-2.0.2}/README.md +0 -0
  14. {utim_cli-2.0.1 → utim_cli-2.0.2}/setup.cfg +0 -0
  15. {utim_cli-2.0.1 → utim_cli-2.0.2}/setup.py +0 -0
  16. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_check_obj.py +0 -0
  17. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_find_mcp.py +0 -0
  18. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_find_plan.py +0 -0
  19. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_find_tool.py +0 -0
  20. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_patch_multimodal.py +0 -0
  21. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/_test_regex.py +0 -0
  22. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/agent.py +0 -0
  23. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/auth.py +0 -0
  24. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/backup.py +0 -0
  25. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/billing.py +0 -0
  26. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/blender_agent.py +0 -0
  27. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/bootstrap.py +0 -0
  28. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/client_utils.py +0 -0
  29. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/config.py +0 -0
  30. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/constants.py +0 -0
  31. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/context_pruner.py +0 -0
  32. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/doctor.py +0 -0
  33. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/harbor.py +0 -0
  34. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/knowledge_graph.py +0 -0
  35. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/local_db.py +0 -0
  36. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/logger.py +0 -0
  37. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/mcp_clean_wrapper.py +0 -0
  38. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/mcp_client.py +0 -0
  39. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/mcp_registry.json +0 -0
  40. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/reflection.py +0 -0
  41. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/report.py +0 -0
  42. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/scrapy_search.py +0 -0
  43. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/__init__.py +0 -0
  44. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/auth.py +0 -0
  45. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/db.py +0 -0
  46. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/about.md +0 -0
  47. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/changelog.md +0 -0
  48. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/docs.md +0 -0
  49. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/features.md +0 -0
  50. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/license.md +0 -0
  51. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/pricing.md +0 -0
  52. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/privacy.md +0 -0
  53. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/refund.md +0 -0
  54. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/support.md +0 -0
  55. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/docs_md/terms.md +0 -0
  56. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/email_utils.py +0 -0
  57. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/exchange_rate.py +0 -0
  58. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/firebase.py +0 -0
  59. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/history.py +0 -0
  60. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/logging_config.py +0 -0
  61. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/models.py +0 -0
  62. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/pricing_updater.py +0 -0
  63. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/rate_limit.py +0 -0
  64. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/router.py +0 -0
  65. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/__init__.py +0 -0
  66. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/auth_routes.py +0 -0
  67. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/completion_routes.py +0 -0
  68. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/credit_routes.py +0 -0
  69. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/feedback_routes.py +0 -0
  70. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/marketplace_routes.py +0 -0
  71. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/quota_routes.py +0 -0
  72. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/quota_share_routes.py +0 -0
  73. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/referral_routes.py +0 -0
  74. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/security_routes.py +0 -0
  75. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/session_routes.py +0 -0
  76. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/routes/share_routes.py +0 -0
  77. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/server.py +0 -0
  78. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/server/storage_nodes.py +0 -0
  79. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/share.py +0 -0
  80. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/share_tui.py +0 -0
  81. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/situational_scoring.py +0 -0
  82. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/state.py +0 -0
  83. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/__init__.py +0 -0
  84. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/feedback_dialog.py +0 -0
  85. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/history_dialog.py +0 -0
  86. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/marketplace_app_state.py +0 -0
  87. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/marketplace_dialog.py +0 -0
  88. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  89. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/mcp_dialog.py +0 -0
  90. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/miniagents_dialog.py +0 -0
  91. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/publish_dialog.py +0 -0
  92. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/quota_dialog.py +0 -0
  93. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  94. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/quota_share_dialog.py +0 -0
  95. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/resume_dialog.py +0 -0
  96. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/skills_dialog.py +0 -0
  97. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/subagents_dialog.py +0 -0
  98. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/tools_dialog.py +0 -0
  99. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/tui/update_dialog.py +0 -0
  100. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/utilities.py +0 -0
  101. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/vector_memory.py +0 -0
  102. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli/workspace.py +0 -0
  103. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/SOURCES.txt +0 -0
  104. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/dependency_links.txt +0 -0
  105. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/entry_points.txt +0 -0
  106. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/requires.txt +0 -0
  107. {utim_cli-2.0.1 → utim_cli-2.0.2}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.2] - 2026-08-04
4
+
5
+ ### 🛠️ Fixes
6
+ - Minor bug fixes.
7
+
3
8
  ## [2.0.1] - 2026-08-04 - Bugfix & Adaptive Iteration Budget
4
9
 
5
10
  ### 🛠️ Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: MIT
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.0.1"
7
+ version = "2.0.2"
8
8
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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.0.1"
8
+ __version__ = "2.0.2"
9
9
 
10
10
  import builtins, os, pathlib, urllib.parse, re
11
11
 
@@ -11,4 +11,4 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.0.1"
14
+ VERSION = "2.0.2"
@@ -3429,6 +3429,7 @@ class Orchestrator:
3429
3429
  results[orig_idx] = (tc, "[Aborted by user]")
3430
3430
  continue
3431
3431
  _tools_module._cancel_event = self.cancel_event
3432
+ _tools_module._active_model_id = self.model_id
3432
3433
  result = self._execute_tool_timed(tc)
3433
3434
  self._current_line_len = 0
3434
3435
  results[orig_idx] = (tc, result)
@@ -3684,81 +3685,6 @@ class Orchestrator:
3684
3685
  self._persist_messages()
3685
3686
  return res
3686
3687
 
3687
- def _filter_active_tools(self, all_tools: List[Dict], messages: List[Dict]) -> List[Dict]:
3688
- """Semantic Intent Tool Masking with Self-Healing Task Re-hydration.
3689
-
3690
- Core coding tools and MCP tools are ALWAYS active. Specialized tools
3691
- (analyze_image, generate_image, plan_project, create_skill, background_tasks)
3692
- are activated via broad semantic intent detection (synonyms, implicit requests,
3693
- file paths) and persistently retained once activated during the task session.
3694
- """
3695
- if not all_tools:
3696
- return all_tools
3697
-
3698
- if not hasattr(self, "_activated_specialized_tools"):
3699
- self._activated_specialized_tools = set()
3700
-
3701
- CORE_TOOLS = {
3702
- "read_file", "write_file", "edit_file", "move_file", "delete_file",
3703
- "run_command", "list_directory", "grep_search", "search", "search_codebase",
3704
- "recall_experience", "store_experience", "manage_memory"
3705
- }
3706
-
3707
- # Broad semantic intent patterns (handles slang, implicit desires, multi-language phrasing)
3708
- SEMANTIC_INTENT_PATTERNS = {
3709
- "analyze_image": [
3710
- r"\.(png|jpg|jpeg|webp|gif|bmp|svg|tiff|ico)\b",
3711
- r"(?i)\b(image|img|screenshot|photo|picture|diagram|fig|figure|ui|mockup|wireframe|visual|inspect|look at|see|show|crop|icon|logo|asset)\b"
3712
- ],
3713
- "generate_image": [
3714
- r"(?i)\b(generate|draw|create|paint|render|make|design|build)\b.*\b(image|picture|photo|illustration|graphic|logo|banner|art|ui|mockup|icon|asset|cover|poster|avatar)\b",
3715
- r"(?i)\b(text to image|image gen|sd|flux|dall-e|midjourney|prompt expander)\b"
3716
- ],
3717
- "plan_project": [
3718
- r"(?i)\b(plan|architect|roadmap|milestone|phases|breakdown|strategy|design system|structure repo|project structure|todo list|task list|multi-step|workflow|steps)\b",
3719
- r"(?i)\b(how should (i|we)|what is the best (way|architecture|structure)|organize this)\b"
3720
- ],
3721
- "create_skill": [
3722
- r"(?i)\b(skill|custom tool|reusable prompt|workflow template|save routine|workspace rule|automation|agent skill)\b"
3723
- ],
3724
- "background_tasks": [
3725
- r"(?i)\b(background|task|bg|daemon|long running|process|job|async|monitor|poll|kill task)\b"
3726
- ]
3727
- }
3728
-
3729
- # Collect text across recent messages (user + assistant reasoning + tool outputs)
3730
- recent_text_parts = []
3731
- for m in (messages[-8:] if messages else []):
3732
- content = m.get("content")
3733
- if isinstance(content, str):
3734
- recent_text_parts.append(content)
3735
- elif isinstance(content, list):
3736
- for part in content:
3737
- if isinstance(part, dict) and part.get("type") == "text":
3738
- recent_text_parts.append(part.get("text", ""))
3739
-
3740
- combined_text = "\n".join(recent_text_parts)
3741
-
3742
- # 1. Semantic intent detection
3743
- for tool_name, patterns in SEMANTIC_INTENT_PATTERNS.items():
3744
- for pat in patterns:
3745
- if re.search(pat, combined_text):
3746
- self._activated_specialized_tools.add(tool_name)
3747
- break
3748
-
3749
- # 2. Re-hydrate if iteration count >= 3 (complex multi-step task automatically gets plan_project)
3750
- if getattr(self, "current_iteration", 0) >= 3:
3751
- self._activated_specialized_tools.add("plan_project")
3752
-
3753
- # 3. Filter all_tools
3754
- filtered = []
3755
- for t in all_tools:
3756
- name = t.get("function", {}).get("name", "")
3757
- if name in CORE_TOOLS or name not in SEMANTIC_INTENT_PATTERNS or name in self._activated_specialized_tools:
3758
- filtered.append(t)
3759
-
3760
- return filtered
3761
-
3762
3688
  # ── Context compression ──────────────────────────────────────────────────
3763
3689
 
3764
3690
  @staticmethod
@@ -4909,30 +4835,6 @@ class Orchestrator:
4909
4835
  # No extra newline here - let the next block handle it
4910
4836
  pass
4911
4837
 
4912
- # ── Self-Healing Re-Activation Gate ──────────────────────────────
4913
- # If native tool_calls are empty and the model expresses inability to find a tool,
4914
- # or mentions a specialized tool by name, immediately unmask ALL specialized tools
4915
- # and re-prompt the model in 0ms so it NEVER gets stuck or stops.
4916
- SPECIALIZED_TOOLS_ALL = {"analyze_image", "generate_image", "plan_project", "create_skill", "background_tasks"}
4917
- if not tool_calls and content and not _attempted_tool_call:
4918
- content_lower = content.lower()
4919
- mentioned_specialized = {t for t in SPECIALIZED_TOOLS_ALL if t in content_lower}
4920
- lacks_tool_signal = any(phrase in content_lower for phrase in [
4921
- "don't have", "do not have", "cannot access", "no tool", "tool is missing",
4922
- "unable to generate", "unable to analyze", "not available", "don't see"
4923
- ])
4924
- if mentioned_specialized or lacks_tool_signal:
4925
- if not hasattr(self, "_activated_specialized_tools"):
4926
- self._activated_specialized_tools = set()
4927
- self._activated_specialized_tools.update(SPECIALIZED_TOOLS_ALL)
4928
-
4929
- self.messages.append({"role": "assistant", "content": content})
4930
- self.messages.append({
4931
- "role": "user",
4932
- "content": "[SYSTEM DIRECTIVE] All specialized tools (analyze_image, generate_image, plan_project, create_skill, background_tasks) have been fully unmasked and loaded into your active tool schema. Please re-run immediately using the required tool."
4933
- })
4934
- continue
4935
-
4936
4838
  # Parse text-based tool calls fallback if native tool calls are empty
4937
4839
  if not tool_calls and content:
4938
4840
  parsed_calls = []
@@ -5520,6 +5422,11 @@ class Orchestrator:
5520
5422
  + result[-tail_chars:]
5521
5423
  + (f"\n\n{_line_hint}" if _line_hint else "")
5522
5424
  )
5425
+
5426
+ # Clear any stale vision images from a previous iteration before
5427
+ # this batch of tools runs (defensive — shouldn't normally be set).
5428
+ _tools_module._pending_vision_images.clear()
5429
+
5523
5430
  # Execute tools in parallel when beneficial
5524
5431
  if len(tool_calls) > 1:
5525
5432
  # Use parallel execution for multiple tools
@@ -5536,50 +5443,14 @@ class Orchestrator:
5536
5443
  func_name = tc.get("function", {}).get("name", "")
5537
5444
  result = _cap_tool_result(func_name, result)
5538
5445
 
5539
- # If result is an [Image: path] marker and model is vision-capable,
5540
- # inject the actual image as multimodal content
5541
- _img_match = __import__("re").match(r"^\[Image:\s+(.+)\]$", result.strip())
5542
- if _img_match and func_name in ("read_file",):
5543
- try:
5544
- from utim_cli.tools import is_model_vision_capable
5545
- if is_model_vision_capable():
5546
- _img_path = _img_match.group(1)
5547
- _multimodal = _tool_result_to_multimodal(result, _img_path)
5548
- self.messages.append(
5549
- {
5550
- "role": "tool",
5551
- "tool_call_id": tc_id,
5552
- "name": func_name,
5553
- "content": _multimodal,
5554
- }
5555
- )
5556
- else:
5557
- self.messages.append(
5558
- {
5559
- "role": "tool",
5560
- "tool_call_id": tc_id,
5561
- "name": func_name,
5562
- "content": result,
5563
- }
5564
- )
5565
- except Exception:
5566
- self.messages.append(
5567
- {
5568
- "role": "tool",
5569
- "tool_call_id": tc_id,
5570
- "name": func_name,
5571
- "content": result,
5572
- }
5573
- )
5574
- else:
5575
- self.messages.append(
5576
- {
5577
- "role": "tool",
5578
- "tool_call_id": tc_id,
5579
- "name": func_name,
5580
- "content": result,
5581
- }
5582
- )
5446
+ self.messages.append(
5447
+ {
5448
+ "role": "tool",
5449
+ "tool_call_id": tc_id,
5450
+ "name": func_name,
5451
+ "content": result,
5452
+ }
5453
+ )
5583
5454
  else:
5584
5455
  # Single tool - execute directly
5585
5456
  for tc in tool_calls:
@@ -5588,55 +5459,41 @@ class Orchestrator:
5588
5459
 
5589
5460
  func_name = tc.get("function", {}).get("name", "")
5590
5461
  _tools_module._cancel_event = self.cancel_event
5462
+ _tools_module._active_model_id = self.model_id
5591
5463
  result = self._execute_tool_timed(tc)
5592
5464
  self._current_line_len = 0
5593
5465
  tc_id = tc.get("id") or str(tc.get("index", "0"))
5594
5466
  result = _cap_tool_result(func_name, result)
5595
5467
 
5596
- # If result is an [Image: path] marker and model is vision-capable,
5597
- # inject the actual image as multimodal content
5598
- _img_match = __import__("re").match(r"^\[Image:\s+(.+)\]$", result.strip())
5599
- if _img_match and func_name in ("read_file",):
5600
- try:
5601
- from utim_cli.tools import is_model_vision_capable
5602
- if is_model_vision_capable():
5603
- _img_path = _img_match.group(1)
5604
- _multimodal = _tool_result_to_multimodal(result, _img_path)
5605
- self.messages.append(
5606
- {
5607
- "role": "tool",
5608
- "tool_call_id": tc_id,
5609
- "name": func_name,
5610
- "content": _multimodal,
5611
- }
5612
- )
5613
- else:
5614
- self.messages.append(
5615
- {
5616
- "role": "tool",
5617
- "tool_call_id": tc_id,
5618
- "name": func_name,
5619
- "content": result,
5620
- }
5621
- )
5622
- except Exception:
5623
- self.messages.append(
5624
- {
5625
- "role": "tool",
5626
- "tool_call_id": tc_id,
5627
- "name": func_name,
5628
- "content": result,
5629
- }
5630
- )
5631
- else:
5632
- self.messages.append(
5633
- {
5634
- "role": "tool",
5635
- "tool_call_id": tc_id,
5636
- "name": func_name,
5637
- "content": result,
5638
- }
5639
- )
5468
+ self.messages.append(
5469
+ {
5470
+ "role": "tool",
5471
+ "tool_call_id": tc_id,
5472
+ "name": func_name,
5473
+ "content": result,
5474
+ }
5475
+ )
5476
+
5477
+ # ── Vision image injection ────────────────────────────────────────
5478
+ # read_file encodes image bytes and pushes them to
5479
+ # _tools_module._pending_vision_images so they bypass the tool-result
5480
+ # string pipeline entirely. Drain the queue here (after ALL tools in
5481
+ # this iteration have run) and inject each image as a "user" message
5482
+ # — the only role that providers universally accept image_url content in.
5483
+ _vision_queue = getattr(_tools_module, "_pending_vision_images", [])
5484
+ if _vision_queue:
5485
+ for _vimg in list(_vision_queue):
5486
+ self.messages.append(
5487
+ {
5488
+ "role": "user",
5489
+ "content": [
5490
+ {"type": "text", "text": f"[Image from read_file: {_vimg['meta']}]"},
5491
+ {"type": "image_url", "image_url": {"url": f"data:{_vimg['mime']};base64,{_vimg['b64']}"}},
5492
+ ],
5493
+ }
5494
+ )
5495
+ _vision_queue.clear()
5496
+
5640
5497
  # --- Repeated Tool Failure Loop Detection ---
5641
5498
  if not hasattr(self, "_tool_failure_counts"):
5642
5499
  self._tool_failure_counts = {}
@@ -159,27 +159,36 @@ def resolve_project_path(filepath: str) -> str:
159
159
  _VISION_MODELS_CACHE = None
160
160
 
161
161
  def is_model_vision_capable(model_id: str = None) -> bool:
162
- """Determine if a model is vision-capable based on models.txt architecture input_modalities."""
162
+ """Determine if a model is vision-capable (Vision: True/False) based on MODEL_REGISTRY and models.txt input_modalities."""
163
163
  global _VISION_MODELS_CACHE
164
164
  if not model_id:
165
- try:
166
- from utim_cli.config import config
167
- model_id = config.get("model") or config.get("active_model") or "google/gemini-3.5-flash-lite"
168
- except Exception:
169
- model_id = "google/gemini-3.5-flash-lite"
165
+ # Prefer the slot injected by the orchestrator; fall back to config as last resort.
166
+ model_id = _active_model_id or ""
167
+ if not model_id:
168
+ try:
169
+ from utim_cli.config import config
170
+ model_id = config.get("main_model") or config.get("model") or ""
171
+ except Exception:
172
+ model_id = ""
170
173
 
171
- model_id_str = str(model_id).lower().strip()
174
+ model_id_str = str(model_id).strip()
175
+ if not model_id_str:
176
+ return False # unknown model → treat as non-vision (safe default)
172
177
 
173
- # Quick keyword match for known vision families
174
- vision_keywords = [
175
- "gemini", "claude", "gpt-5", "gpt-4", "grok", "qwen3.7", "qwen3.6",
176
- "inkling", "kimi", "muse-spark", "krea", "vision", "-vl", "llava", "pixtral"
177
- ]
178
- if any(kw in model_id_str for kw in vision_keywords):
179
- if not any(non_v in model_id_str for non_v in ["text-only", "coder-air", "kat-coder"]):
180
- return True
178
+ model_id_lower = model_id_str.lower()
179
+
180
+ # 1. Check server MODEL_REGISTRY first
181
+ try:
182
+ from utim_cli.server.models import MODEL_REGISTRY
183
+ if model_id_str in MODEL_REGISTRY:
184
+ return MODEL_REGISTRY[model_id_str].vision
185
+ for k, entry in MODEL_REGISTRY.items():
186
+ if k.lower() == model_id_lower:
187
+ return entry.vision
188
+ except Exception:
189
+ pass
181
190
 
182
- # Lazy-load models.txt mapping
191
+ # 2. Build / query models.txt cache (authoritative list from OpenRouter metadata)
183
192
  if _VISION_MODELS_CACHE is None:
184
193
  _VISION_MODELS_CACHE = {}
185
194
  try:
@@ -190,15 +199,27 @@ def is_model_vision_capable(model_id: str = None) -> bool:
190
199
  models_txt_path = "models.txt"
191
200
  if os.path.exists(models_txt_path):
192
201
  with open(models_txt_path, "r", encoding="utf-8") as f:
193
- data = json.load(f).get("data", [])
194
- for m in data:
195
- mid = m.get("id", "").lower()
196
- in_mods = m.get("architecture", {}).get("input_modalities", [])
197
- _VISION_MODELS_CACHE[mid] = ("image" in in_mods)
202
+ data = json.load(f)
203
+ items = data.get("data", []) if isinstance(data, dict) else data
204
+ for m in items:
205
+ mid = m.get("id", "")
206
+ if not mid:
207
+ continue
208
+ in_mods = m.get("architecture", {}).get("input_modalities", []) or []
209
+ _VISION_MODELS_CACHE[mid.lower()] = ("image" in in_mods)
198
210
  except Exception:
199
211
  _VISION_MODELS_CACHE = {}
200
212
 
201
- return _VISION_MODELS_CACHE.get(model_id_str, False)
213
+ if model_id_lower in _VISION_MODELS_CACHE:
214
+ return _VISION_MODELS_CACHE[model_id_lower]
215
+
216
+ # 3. Fallback for custom / unlisted model names
217
+ vision_keywords = ["vision", "-vl", "llava", "pixtral", "image", "multimodal", "gpt-4o", "gemini", "claude-3", "claude-4", "claude-5", "qwen3"]
218
+ if any(kw in model_id_lower for kw in vision_keywords):
219
+ if not any(non_v in model_id_lower for non_v in ["text-only", "coder-air", "kat-coder"]):
220
+ return True
221
+
222
+ return False
202
223
 
203
224
 
204
225
  def _extract_file_symbols(filepath: str, file_content: str) -> list:
@@ -305,15 +326,125 @@ def read_file(filepath: str, start_line: int = None, end_line: int = None, symbo
305
326
  ext = _os.path.splitext(filepath)[1].lower()
306
327
  image_extensions = {'.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp'}
307
328
  if ext in image_extensions:
308
- # Vision-capable models can see the image directly no need for a text
309
- # description that clutters the tool indicator. Non-vision models still
310
- # need the description as their only way to "see" the image.
329
+ # Collect image metadata (size, dimensions) for the text portion of the response.
311
330
  try:
312
- if is_model_vision_capable():
313
- return f"[Image: {filepath}]"
331
+ file_size = _os.path.getsize(filepath)
332
+ size_kb = file_size / 1024.0
333
+ size_str = f"{size_kb:.1f} KB" if size_kb < 1024 else f"{size_kb/1024:.2f} MB"
334
+ except Exception:
335
+ size_str = "unknown size"
336
+
337
+ meta = f"[Image: {_os.path.basename(filepath)} | {ext.lstrip('.').upper()} | {size_str} | URI: {_make_file_uri(filepath)}]"
338
+
339
+ # Try to also read image dimensions (pure stdlib where possible, else Pillow)
340
+ dim_str = ""
341
+ try:
342
+ with open(filepath, "rb") as _f:
343
+ _head = _f.read(64)
344
+ # PNG: width@16, height@20 (big-endian uint32)
345
+ if _head.startswith(b"\x89PNG\r\n\x1a\n") and len(_head) >= 24:
346
+ import struct
347
+ w, h = struct.unpack(">II", _head[16:24])
348
+ dim_str = f" | {w}x{h}px"
349
+ # GIF: width@6, height@8 (little-endian uint16)
350
+ elif _head.startswith(b"GIF8") and len(_head) >= 10:
351
+ import struct
352
+ w, h = struct.unpack("<HH", _head[6:10])
353
+ dim_str = f" | {w}x{h}px"
354
+ # JPEG: parse SOF marker to find dimensions
355
+ elif _head.startswith(b"\xff\xd8\xff"):
356
+ import struct
357
+ _f2 = open(filepath, "rb")
358
+ try:
359
+ soi = _f2.read(2)
360
+ while True:
361
+ marker = _f2.read(2)
362
+ if len(marker) < 2:
363
+ break
364
+ if marker[0] != 0xFF:
365
+ break
366
+ # Skip SOI / EOI / RST markers
367
+ if marker[1] in (0xD8, 0xD9) or 0xD0 <= marker[1] <= 0xD7:
368
+ continue
369
+ seg_len_bytes = _f2.read(2)
370
+ if len(seg_len_bytes) < 2:
371
+ break
372
+ seg_len = struct.unpack(">H", seg_len_bytes)[0]
373
+ # SOFn markers (Start of Frame): 0xC0-0xCF except 0xC4,0xC8,0xCC
374
+ if 0xC0 <= marker[1] <= 0xCF and marker[1] not in (0xC4, 0xC8, 0xCC):
375
+ _f2.read(1) # precision
376
+ h, w = struct.unpack(">HH", _f2.read(4))
377
+ dim_str = f" | {w}x{h}px"
378
+ break
379
+ else:
380
+ _f2.read(seg_len - 2)
381
+ finally:
382
+ _f2.close()
383
+ # WebP: 'RIFF....WEBP' then VP8 / VP8L / VP8X chunk
384
+ elif _head.startswith(b"RIFF") and _head[8:12] == b"WEBP":
385
+ with open(filepath, "rb") as _f3:
386
+ _f3.read(12)
387
+ chunk_hdr = _f3.read(4)
388
+ if chunk_hdr == b"VP8 ":
389
+ _f3.read(10) # frame tag + start code
390
+ w = int.from_bytes(_f3.read(2), "little") & 0x3FFF
391
+ h = int.from_bytes(_f3.read(2), "little") & 0x3FFF
392
+ dim_str = f" | {w}x{h}px"
393
+ elif chunk_hdr == b"VP8L":
394
+ _f3.read(1) # signature byte
395
+ b1, b2, b3, b4 = _f3.read(4)
396
+ w = ((b2 & 0x3F) << 8 | b1) + 1
397
+ h = (((b4 & 0x0F) << 10) | (b3 << 2) | ((b2 & 0xC0) >> 6)) + 1
398
+ dim_str = f" | {w}x{h}px"
399
+ elif chunk_hdr == b"VP8X":
400
+ _f3.read(8)
401
+ w = int.from_bytes(_f3.read(3), "little") + 1
402
+ h = int.from_bytes(_f3.read(3), "little") + 1
403
+ dim_str = f" | {w}x{h}px"
314
404
  except Exception:
315
405
  pass
316
- return analyze_image(filepath, "Identify and describe this image in detail, listing any people, actors, characters, text, UI elements, colors, and visual layout.")
406
+
407
+ meta += dim_str
408
+
409
+ # Route image to model based on vision capability:
410
+ #
411
+ # Vision-capable model → encode bytes here, push to _pending_vision_images.
412
+ # The orchestrator drains this queue after all tools complete and injects a
413
+ # "user" message with the raw base64 image so the model truly sees it.
414
+ # (image_url blocks are NOT supported inside "tool" role messages by most
415
+ # providers — they must live in a "user" message.)
416
+ #
417
+ # Non-vision model → call analyze_image() in the background and return its
418
+ # text description so the model has useful context.
419
+ if is_model_vision_capable():
420
+ import base64 as _b64mod, mimetypes as _mtmod
421
+ _mime_v, _ = _mtmod.guess_type(filepath)
422
+ _ext_v = _os.path.splitext(filepath)[1].lower()
423
+ if not _mime_v or not _mime_v.startswith("image/"):
424
+ _mime_v = f"image/{_ext_v[1:]}"
425
+ if _ext_v == ".jpg":
426
+ _mime_v = "image/jpeg"
427
+ try:
428
+ with open(filepath, "rb") as _fv:
429
+ _b64_v = _b64mod.b64encode(_fv.read()).decode("utf-8")
430
+ # Push to queue — orchestrator injects the user message after tools complete
431
+ _pending_vision_images.append({"meta": meta, "b64": _b64_v, "mime": _mime_v})
432
+ return meta # return clean metadata text only; no sentinel needed
433
+ except Exception as _venc_err:
434
+ # Encoding failed — fall through to analyze_image text fallback below
435
+ pass
436
+
437
+ # Non-vision model (or encoding failed): describe image via external vision sub-model.
438
+ try:
439
+ description = analyze_image(
440
+ filepath,
441
+ "Identify and describe this image in detail, listing any people, actors, characters, "
442
+ "text, UI elements, colors, visual layout, code or terminal output shown, and any "
443
+ "visible error messages or stack traces verbatim."
444
+ )
445
+ return f"{meta}\n{description}"
446
+ except Exception as _e2:
447
+ return f"{meta}\n(Vision description unavailable: {_e2})"
317
448
 
318
449
  try:
319
450
  with open(filepath, "rb") as f:
@@ -967,6 +1098,17 @@ def edit_file(filepath: str, old_str: str = None, new_str: str = None, replaceme
967
1098
  # threading.Event here before each tool call so run_command can be aborted.
968
1099
  _cancel_event = None # type: threading.Event | None
969
1100
 
1101
+ # Active model ID slot — injected by the orchestrator before each tool call
1102
+ # (same pattern as _cancel_event). read_file reads this to decide whether to
1103
+ # push image bytes to _pending_vision_images or call analyze_image instead.
1104
+ _active_model_id: str = ""
1105
+
1106
+ # Queue for vision images: read_file pushes here when the active model is
1107
+ # vision-capable; the orchestrator drains it after each tool-call batch and
1108
+ # injects a "user" message containing the raw image bytes so the model can
1109
+ # actually see the image (most providers reject image_url in "tool" messages).
1110
+ _pending_vision_images: list = [] # list of {"meta": str, "b64": str, "mime": str}
1111
+
970
1112
  # ─── Intelligent Sandbox mode ──────────────────────────────────────────────────
971
1113
  # Set to True via --sandbox CLI flag. When active, run_command uses static analysis
972
1114
  # to check commands for safety. Risky commands are blocked unless approved.
@@ -621,55 +621,53 @@ def _dialog_model_main(orchestrator, target="main"):
621
621
  if is_paid_plan:
622
622
  approved_set.update({
623
623
  "anthropic/claude-sonnet-4.6",
624
+ "anthropic/claude-sonnet-4.5",
625
+ "anthropic/claude-sonnet-5",
626
+ "anthropic/claude-opus-4.5",
627
+ "anthropic/claude-opus-4.6",
628
+ "anthropic/claude-opus-4.7",
629
+ "anthropic/claude-opus-4.8",
630
+ "anthropic/claude-fable-5",
624
631
  "inclusionai/ling-2.6-flash",
632
+ "inclusionai/ling-2.6-1t",
625
633
  "xiaomi/mimo-v2.5",
626
634
  "xiaomi/mimo-v2.5-pro",
627
635
  "deepseek/deepseek-v4-flash",
636
+ "deepseek/deepseek-v4-flash-0731",
628
637
  "deepseek/deepseek-v4-pro",
638
+ "deepseek/deepseek-r1",
629
639
  "openai/gpt-5.5",
630
- "inclusionai/ling-2.6-1t",
631
- "moonshotai/kimi-k2.6",
640
+ "openai/gpt-5.4",
641
+ "openai/gpt-5.4-mini",
632
642
  "openai/gpt-5.3-codex",
643
+ "moonshotai/kimi-k2.6",
644
+ "moonshotai/kimi-k2.7-code",
645
+ "moonshotai/kimi-k2.5",
646
+ "moonshotai/kimi-k3",
633
647
  "google/gemini-3.1-pro-preview-customtools",
634
- "openai/gpt-5.4",
648
+ "google/gemini-3.5-flash",
649
+ "google/gemini-3.6-flash",
635
650
  "minimax/minimax-m2.7",
651
+ "minimax/minimax-m2.5",
652
+ "minimax/minimax-m3",
636
653
  "kwaipilot/kat-coder-pro-v2",
637
654
  "z-ai/glm-5.1",
638
- "anthropic/claude-fable-5",
639
- "nex-agi/nex-n2-pro",
640
- "minimax/minimax-m3",
641
- "moonshotai/kimi-k2.7-code",
642
- "deepseek/deepseek-r1",
643
- "x-ai/grok-4.3",
644
- "google/gemini-3.5-flash",
645
- "qwen/qwen3.7-max",
646
- "stepfun/step-3.7-flash",
647
- "thinkingmachines/inkling",
648
- "moonshotai/kimi-k3",
649
- "meta/muse-spark-1.1",
650
-
651
- "anthropic/claude-sonnet-4.5",
652
- "anthropic/claude-sonnet-4.6",
653
- "anthropic/claude-opus-4.5",
654
- "anthropic/claude-opus-4.6",
655
- "anthropic/claude-opus-4.7",
656
- "anthropic/claude-opus-4.8",
657
- "anthropic/claude-sonnet-5",
658
655
  "z-ai/glm-5-turbo",
659
656
  "z-ai/glm-4.7",
660
657
  "z-ai/glm-5",
661
658
  "z-ai/glm-5.2",
662
- "qwen/qwen3.7-plus",
663
- "qwen/qwen3.7-max",
664
- "qwen/qwen3.6-plus",
665
- "openai/gpt-5.4-mini",
666
- "minimax/minimax-m2.5",
659
+ "nex-agi/nex-n2-pro",
660
+ "x-ai/grok-4.3",
667
661
  "x-ai/grok-4.20",
668
662
  "x-ai/grok-build-0.1",
669
- "moonshotai/kimi-k2.5",
670
- "google/gemini-3.6-flash",
663
+ "qwen/qwen3.7-max",
664
+ "qwen/qwen3.7-plus",
665
+ "qwen/qwen3.6-plus",
666
+ "stepfun/step-3.7-flash",
667
+ "thinkingmachines/inkling",
668
+ "meta/muse-spark-1.1",
671
669
  "poolside/laguna-s-2.1:free",
672
- })
670
+ })
673
671
  elif target.startswith('subagent_') and target not in ('subagent_image_gen', 'subagent_blender_vision', 'subagent_analyze_image', 'subagent_blender_code'):
674
672
  approved_set = {
675
673
  DEFAULT_MODEL,
@@ -818,6 +816,7 @@ def _dialog_model_main(orchestrator, target="main"):
818
816
  "xiaomi/mimo-v2.5": (0, "Very low"),
819
817
  "xiaomi/mimo-v2.5-pro": (2, "Medium"),
820
818
  "deepseek/deepseek-v4-flash": (0, "Very low"),
819
+ "deepseek/deepseek-v4-flash-0731": (0, "Very low"),
821
820
  "deepseek/deepseek-v4-pro": (1, "Low"),
822
821
  "openai/gpt-5.5": (4, "Very high"),
823
822
  "inclusionai/ling-2.6-1t": (1, "Low"),
@@ -907,6 +906,7 @@ def _dialog_model_main(orchestrator, target="main"):
907
906
  "xiaomi/mimo-v2.5": ("Highly capable multimodal model by Xiaomi.", []),
908
907
  "xiaomi/mimo-v2.5-pro": ("Xiaomi flagship multimodal and reasoning model.", []),
909
908
  "deepseek/deepseek-v4-flash": ("Ultra-fast, cost-effective model by DeepSeek.", []),
909
+ "deepseek/deepseek-v4-flash-0731": ("DeepSeek V4 Flash 0731 — Ultra-fast MoE coding and reasoning model by DeepSeek.", []),
910
910
  "deepseek/deepseek-v4-pro": ("DeepSeek flagship MoE and reasoning model.", []),
911
911
  "openai/gpt-5.5": ("Next-gen frontier reasoning model by OpenAI.", []),
912
912
  "inclusionai/ling-2.6-1t": ("Large context agent model by InclusionAI.", []),
@@ -953,9 +953,7 @@ def _dialog_model_main(orchestrator, target="main"):
953
953
  "google/gemini-3.1-flash-image": ("Google stable image and multimodal vision model.", []),
954
954
  "openai/gpt-5-image-mini": ("OpenAI high-speed multimodal and image model.", []),
955
955
  "openai/gpt-image-2": ("OpenAI advanced image synthesis and editing model.", []),
956
-
957
956
  "anthropic/claude-sonnet-4.5": ("Anthropic: Claude Sonnet 4.5.", []),
958
- "anthropic/claude-sonnet-4.6": ("Anthropic: Claude Sonnet 4.6.", []),
959
957
  "anthropic/claude-opus-4.5": ("Anthropic: Claude Opus 4.5.", []),
960
958
  "anthropic/claude-opus-4.6": ("Anthropic: Claude Opus 4.6.", []),
961
959
  "anthropic/claude-opus-4.7": ("Anthropic: Claude Opus 4.7.", []),
@@ -966,7 +964,6 @@ def _dialog_model_main(orchestrator, target="main"):
966
964
  "z-ai/glm-5": ("Z.ai: GLM 5.", []),
967
965
  "z-ai/glm-5.2": ("Z.ai: GLM 5.2.", []),
968
966
  "qwen/qwen3.7-plus": ("Qwen: Qwen3.7 Plus.", []),
969
- "qwen/qwen3.7-max": ("Qwen: Qwen3.7 Max.", []),
970
967
  "qwen/qwen3.6-plus": ("Qwen: Qwen3.6 Plus.", []),
971
968
  "openai/gpt-5.4-mini": ("OpenAI: GPT-5.4 Mini.", []),
972
969
  "minimax/minimax-m2.5": ("MiniMax: MiniMax M2.5.", []),
@@ -1334,6 +1331,8 @@ def _dialog_model_main(orchestrator, target="main"):
1334
1331
  display_id = "Krea 2 image"
1335
1332
  elif display_id == "gemini-3.1-pro-preview-customtools":
1336
1333
  display_id = "gemini-3.1-pro-preview"
1334
+ elif "qwen3-next-80b" in display_id.lower() or "qwen3-next-80b" in mid.lower():
1335
+ display_id = "Qwen3 Next 80B"
1337
1336
  else:
1338
1337
  display_id = display_id.replace('-', ' ')
1339
1338
  if display_id:
@@ -1348,6 +1347,11 @@ def _dialog_model_main(orchestrator, target="main"):
1348
1347
 
1349
1348
  desc = m.get("desc", "Fast, efficient model for daily tasks.")
1350
1349
 
1350
+ # Determine explicit vision capability from models.txt / MODEL_REGISTRY
1351
+ from utim_cli.tools import is_model_vision_capable
1352
+ vis_tag = "[Vision: True]" if is_model_vision_capable(mid) else "[Vision: False]"
1353
+ full_desc = f"{vis_tag} {desc}"
1354
+
1351
1355
  style = bg or ('fg:#f9e2af' if source == 'custom' else 'fg:#cdd6f4')
1352
1356
  desc_style = bg or 'fg:#585b70'
1353
1357
 
@@ -1374,7 +1378,7 @@ def _dialog_model_main(orchestrator, target="main"):
1374
1378
  cost_style = "fg:#f38ba8"
1375
1379
 
1376
1380
  desc_width = max(24, min(54, (console.size.width or 100) - 62))
1377
- desc_lines = _compact_desc(desc, desc_width)
1381
+ desc_lines = _compact_desc(full_desc, desc_width)
1378
1382
  desc_text = f" {desc_lines[0]}{current}\n"
1379
1383
  for extra in desc_lines[1:]:
1380
1384
  desc_text += f"{'':<55}{extra}\n"
@@ -1388,7 +1392,7 @@ def _dialog_model_main(orchestrator, target="main"):
1388
1392
  ]
1389
1393
  else:
1390
1394
  desc_width = max(24, min(72, (console.size.width or 100) - 10))
1391
- desc_lines = _compact_desc(desc, desc_width)
1395
+ desc_lines = _compact_desc(full_desc, desc_width)
1392
1396
  desc_text = "".join(f" {line}\n" for line in desc_lines) + "\n"
1393
1397
  return [
1394
1398
  (style, f" {display_id}"),
@@ -1196,7 +1196,7 @@ def _print_animated_banner(animated: bool = True):
1196
1196
  else:
1197
1197
  console.print(f"[bold {color}]{BANNER_BIG}[/bold {color}]")
1198
1198
 
1199
- console.print(f" [bold white]U Think I Make[/bold white] [bold #cba6f7]v2.0.1[/bold #cba6f7]")
1199
+ console.print(f" [bold white]U Think I Make[/bold white] [bold #cba6f7]v2.0.2[/bold #cba6f7]")
1200
1200
  user_email = config.email
1201
1201
  is_logged_in = bool(config.token and user_email and user_email.upper() != "GUEST")
1202
1202
  user_type = "Guest Mode" if not is_logged_in else "UTIM Community"
@@ -2003,7 +2003,7 @@ def _run_mcp_search_list_dialog(rows, render_row_fn, title="", legend="", search
2003
2003
  return [
2004
2004
  ('', '\n'),
2005
2005
  ('bold #f38ba8', ' ⚠️ No matching MCP servers found.\n'),
2006
- ('dim', ' Try searching for different keywords (e.g. database, search, maps, slack).\n')
2006
+ ('class:dim', ' Try searching for different keywords (e.g. database, search, maps, slack).\n')
2007
2007
  ]
2008
2008
 
2009
2009
  matching_heights = [row_heights[orig_idx] for orig_idx, _ in filtered_rows]
@@ -4686,7 +4686,7 @@ def check_and_update_background():
4686
4686
  return
4687
4687
 
4688
4688
  latest_ver = resp.json().get("version", "")
4689
- current_ver = "2.0.1"
4689
+ current_ver = "2.0.2"
4690
4690
 
4691
4691
  def parse_ver(v):
4692
4692
  return [int(x) for x in v.split(".") if x.isdigit()]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://utim.dev
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes