ayder-cli 2.2.7__tar.gz → 2.2.8__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 (267) hide show
  1. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/PKG-INFO +1 -1
  2. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/config.toml.example +14 -12
  3. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/pyproject.toml +1 -1
  4. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/config.py +24 -1
  5. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama.py +106 -3
  6. ayder_cli-2.2.8/src/ayder_cli/tui/rendering.py +12 -0
  7. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/widgets.py +13 -11
  8. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_v2.py +33 -0
  9. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_provider_integration.py +109 -0
  10. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_status_bar_badges.py +8 -0
  11. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_widgets.py +52 -1
  12. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/.github/workflows/ci.yml +0 -0
  13. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/.github/workflows/python-package.yml +0 -0
  14. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/.gitignore +0 -0
  15. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/.python-version +0 -0
  16. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/AGENTS.md +0 -0
  17. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/LICENSE +0 -0
  18. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/README.md +0 -0
  19. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/PROJECT_STRUCTURE.md +0 -0
  20. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/README_AGENT_WORKFLOW.md +0 -0
  21. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/README_OLLAMA.md +0 -0
  22. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/cc.png +0 -0
  23. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/superpowers/plans/2026-06-26-file-editor-improvements.md +0 -0
  24. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/superpowers/plans/2026-06-26-session-save-resume.md +0 -0
  25. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/superpowers/specs/2026-06-26-file-editor-improvements-design.md +0 -0
  26. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/superpowers/specs/2026-06-26-session-save-resume-design.md +0 -0
  27. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/__init__.py +0 -0
  28. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/__main__.py +0 -0
  29. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/__init__.py +0 -0
  30. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/callbacks.py +0 -0
  31. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/config.py +0 -0
  32. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/registry.py +0 -0
  33. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/run.py +0 -0
  34. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/runner.py +0 -0
  35. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/tool.py +0 -0
  36. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/worktree.py +0 -0
  37. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/README.md +0 -0
  38. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/__init__.py +0 -0
  39. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/execution_policy.py +0 -0
  40. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/message_contract.py +0 -0
  41. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/runtime_factory.py +0 -0
  42. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/validation.py +0 -0
  43. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/cli.py +0 -0
  44. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/cli_callbacks.py +0 -0
  45. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/cli_runner.py +0 -0
  46. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/console.py +0 -0
  47. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/cache_monitor.py +0 -0
  48. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/config_migration.py +0 -0
  49. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/context.py +0 -0
  50. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/context_manager.py +0 -0
  51. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/context_manager_factory.py +0 -0
  52. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/default_context_manager.py +0 -0
  53. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/ollama_context_manager.py +0 -0
  54. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/result.py +0 -0
  55. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/session.py +0 -0
  56. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/logging_config.py +0 -0
  57. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/loops/__init__.py +0 -0
  58. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/loops/chat_loop.py +0 -0
  59. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/parser.py +0 -0
  60. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/process_manager.py +0 -0
  61. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/prompts.py +0 -0
  62. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/__init__.py +0 -0
  63. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/base.py +0 -0
  64. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/claude.py +0 -0
  65. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/deepseek.py +0 -0
  66. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/gemini.py +0 -0
  67. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/glm.py +0 -0
  68. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/__init__.py +0 -0
  69. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/_errors.py +0 -0
  70. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/base.py +0 -0
  71. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/deepseek.py +0 -0
  72. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/generic_native.py +0 -0
  73. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/generic_xml.py +0 -0
  74. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/matrix.py +0 -0
  75. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/minimax.py +0 -0
  76. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/qwen3.py +0 -0
  77. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/registry.py +0 -0
  78. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_inspector.py +0 -0
  79. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/openai.py +0 -0
  80. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/qwen.py +0 -0
  81. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/orchestrator.py +0 -0
  82. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/retry.py +0 -0
  83. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/services/__init__.py +0 -0
  84. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/services/interactions.py +0 -0
  85. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/themes/__init__.py +0 -0
  86. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/themes/ayder.py +0 -0
  87. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/themes/claude.py +0 -0
  88. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/__init__.py +0 -0
  89. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/__init__.py +0 -0
  90. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/background_process_tool.py +0 -0
  91. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/context.py +0 -0
  92. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/context_definitions.py +0 -0
  93. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/filesystem.py +0 -0
  94. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/filesystem_definitions.py +0 -0
  95. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/note_tool.py +0 -0
  96. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/notes.py +0 -0
  97. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/notes_definitions.py +0 -0
  98. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/process_manager_definitions.py +0 -0
  99. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/search.py +0 -0
  100. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/search_definitions.py +0 -0
  101. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/shell.py +0 -0
  102. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/shell_definitions.py +0 -0
  103. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/skill.py +0 -0
  104. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/skill_definitions.py +0 -0
  105. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/task_definitions.py +0 -0
  106. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/task_tool.py +0 -0
  107. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/tasks.py +0 -0
  108. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/utils_tools.py +0 -0
  109. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/utils_tools_definitions.py +0 -0
  110. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/web.py +0 -0
  111. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/web_definitions.py +0 -0
  112. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/definition.py +0 -0
  113. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/execution.py +0 -0
  114. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/hooks.py +0 -0
  115. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/normalization.py +0 -0
  116. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/plugin_api.py +0 -0
  117. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/plugin_github.py +0 -0
  118. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/plugin_manager.py +0 -0
  119. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/plugin_status.py +0 -0
  120. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/registry.py +0 -0
  121. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/schemas.py +0 -0
  122. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/utils.py +0 -0
  123. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/__init__.py +0 -0
  124. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/adapter.py +0 -0
  125. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/app.py +0 -0
  126. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/commands.py +0 -0
  127. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/helpers.py +0 -0
  128. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/keybindings.py +0 -0
  129. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/parser.py +0 -0
  130. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/screens.py +0 -0
  131. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/theme_manager.py +0 -0
  132. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/types.py +0 -0
  133. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/ui.py +0 -0
  134. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/version.py +0 -0
  135. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/COVERAGE.md +0 -0
  136. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/__init__.py +0 -0
  137. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/__init__.py +0 -0
  138. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_agent_consolidated.py +0 -0
  139. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_agent_tools.py +0 -0
  140. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_callbacks.py +0 -0
  141. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_config.py +0 -0
  142. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_integration.py +0 -0
  143. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_queue.py +0 -0
  144. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_registry.py +0 -0
  145. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_run.py +0 -0
  146. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_runner.py +0 -0
  147. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_runner_provider_unavailable.py +0 -0
  148. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_tool.py +0 -0
  149. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_worktree_helpers.py +0 -0
  150. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_worktree_isolation.py +0 -0
  151. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/application/test_message_contract.py +0 -0
  152. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/application/test_runtime_factory.py +0 -0
  153. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/client/test_main.py +0 -0
  154. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/convergence/__init__.py +0 -0
  155. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/convergence/test_execution_policy_parity.py +0 -0
  156. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/convergence/test_runtime_wiring.py +0 -0
  157. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/convergence/test_validation_path.py +0 -0
  158. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_cache_monitor.py +0 -0
  159. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config.py +0 -0
  160. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_coverage.py +0 -0
  161. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_migration.py +0 -0
  162. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_provider_logging.py +0 -0
  163. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_retry.py +0 -0
  164. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_context_manager_factory.py +0 -0
  165. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_default_context_manager.py +0 -0
  166. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_default_context_manager_should_compact.py +0 -0
  167. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_ollama_context_manager.py +0 -0
  168. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_ollama_context_manager_cache_monitor.py +0 -0
  169. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_ollama_context_manager_max_history.py +0 -0
  170. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_parameter_aliasing.py +0 -0
  171. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_parser.py +0 -0
  172. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_session.py +0 -0
  173. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/loops/__init__.py +0 -0
  174. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/loops/test_base.py +0 -0
  175. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/loops/test_chat_loop_hook.py +0 -0
  176. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/loops/test_chat_loop_tool_truncation.py +0 -0
  177. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/__init__.py +0 -0
  178. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/__init__.py +0 -0
  179. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_base.py +0 -0
  180. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_deepseek.py +0 -0
  181. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_errors.py +0 -0
  182. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_fallback.py +0 -0
  183. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_generic_native.py +0 -0
  184. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_generic_xml.py +0 -0
  185. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_matrix.py +0 -0
  186. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_minimax.py +0 -0
  187. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_qwen3.py +0 -0
  188. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_registry.py +0 -0
  189. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_glm_async.py +0 -0
  190. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_inspector.py +0 -0
  191. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_native.py +0 -0
  192. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_xml.py +0 -0
  193. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_xml_autoroute.py +0 -0
  194. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_xml_usage.py +0 -0
  195. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_orchestrator_capabilities.py +0 -0
  196. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_qwen_async.py +0 -0
  197. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_retry_classifier.py +0 -0
  198. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_retry_integration.py +0 -0
  199. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_retrying_provider.py +0 -0
  200. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/services/test_boundary.py +0 -0
  201. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/services/test_interaction_sink.py +0 -0
  202. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_cli.py +0 -0
  203. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_cli_callbacks.py +0 -0
  204. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_context.py +0 -0
  205. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_env_manager.py +0 -0
  206. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_logging_config.py +0 -0
  207. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_notes.py +0 -0
  208. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_process_manager.py +0 -0
  209. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_tasks.py +0 -0
  210. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/__init__.py +0 -0
  211. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_agent_notes.py +0 -0
  212. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_background_process_tool.py +0 -0
  213. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_bash_tool.py +0 -0
  214. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_definition_discovery.py +0 -0
  215. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_di_injection.py +0 -0
  216. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_impl.py +0 -0
  217. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_impl_coverage.py +0 -0
  218. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_note_tool.py +0 -0
  219. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_notes_api.py +0 -0
  220. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_path_security.py +0 -0
  221. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_api.py +0 -0
  222. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_github.py +0 -0
  223. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_loading.py +0 -0
  224. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_manager.py +0 -0
  225. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_status.py +0 -0
  226. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_registry.py +0 -0
  227. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_registry_coverage.py +0 -0
  228. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_result.py +0 -0
  229. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_schemas.py +0 -0
  230. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_search_codebase.py +0 -0
  231. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_skill_tool.py +0 -0
  232. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_task_tool.py +0 -0
  233. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_utils.py +0 -0
  234. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_web.py +0 -0
  235. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/__init__.py +0 -0
  236. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_agent_list_screen.py +0 -0
  237. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_do_clear_coordination.py +0 -0
  238. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_follow_mode.py +0 -0
  239. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_help_screen.py +0 -0
  240. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_input_box.py +0 -0
  241. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_keybindings.py +0 -0
  242. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_multi_select_screen.py +0 -0
  243. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_multiline_input.py +0 -0
  244. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_paste.py +0 -0
  245. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_pending_compact_consumer.py +0 -0
  246. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_pull_delivery.py +0 -0
  247. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_resume.py +0 -0
  248. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_themes.py +0 -0
  249. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_turn_consumer.py +0 -0
  250. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_confirm_screen.py +0 -0
  251. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_diff_preview.py +0 -0
  252. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_plugin_autoenable.py +0 -0
  253. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_provider_switch.py +0 -0
  254. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_run_tui_startup.py +0 -0
  255. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_assistant_sanitization.py +0 -0
  256. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_chat_loop.py +0 -0
  257. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_agent.py +0 -0
  258. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_logging.py +0 -0
  259. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_plugin.py +0 -0
  260. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_provider.py +0 -0
  261. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_skill.py +0 -0
  262. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_helpers.py +0 -0
  263. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_temporal_command.py +0 -0
  264. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_widgets.py +0 -0
  265. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_ui.py +0 -0
  266. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_ui_coverage.py +0 -0
  267. {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_ui_verbose.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ayder-cli
3
- Version: 2.2.7
3
+ Version: 2.2.8
4
4
  Summary: AI agent for any LLMs
5
5
  Project-URL: Homepage, https://github.com/ayder/ayder-cli
6
6
  Project-URL: Repository, https://github.com/ayder/ayder-cli.git
@@ -48,11 +48,12 @@
48
48
  # `export OLLAMA_API_KEY="<key>"`. The api_key config field is INERT on the
49
49
  # native path, so a missing env var ⇒ 401 Unauthorized.
50
50
  #
51
- # NOTE on `think`: on the native `ollama` driver, ayder hardcodes thinking ON
52
- # (think=True) and does NOT forward the configured level — so a
53
- # `think = "high"` below documents intent, but the "low/medium/high"
54
- # granularity is not applied. The `openai` driver sends no reasoning-effort
55
- # parameter at all.
51
+ # NOTE on `think`: on the native `ollama` driver, ayder defaults thinking ON
52
+ # (think = true) and forwards either a boolean or Ollama's "low",
53
+ # "medium", "high" levels. If a selected model rejects thinking mode,
54
+ # ayder retries that request once with think=false. Set `think = false`
55
+ # in an [llm.*] profile for known non-thinking models. The `openai`
56
+ # driver sends no reasoning-effort parameter at all.
56
57
  #
57
58
  # NOTE on tuning knobs — which actually take effect depends on the DRIVER. These
58
59
  # agents run on the native `ollama` driver, which ignores most of them:
@@ -60,17 +61,16 @@
60
61
  # • max_output_tokens — NOT sent on the ollama path (Ollama sizes
61
62
  # generation itself); honored only by the
62
63
  # openai-family drivers as max_tokens. Default 4096.
63
- # • think — hardcoded ON for ollama (level ignored); openai
64
- # sends none.
64
+ # • think — sent on the ollama path (default true; accepts
65
+ # false or "low"/"medium"/"high"); openai sends
66
+ # none.
65
67
  # • num_ctx — on ollama it is provisional only (the real window
66
68
  # is auto-detected from /api/show); on the openai
67
69
  # path it sizes the context-manager budget.
68
70
  # Default 65536.
69
- # The per-agent temperature / max_output_tokens / think values below are
70
- # therefore largely INERT on this Ollama pipeline — they are kept to
71
- # document intent and to carry over if you switch an agent to an `openai`
72
- # profile. To actually tune these, use the `openai` driver — see
73
- # [llm.openai] below.
71
+ # The per-agent temperature / max_output_tokens values below are largely
72
+ # INERT on this Ollama pipeline; per-agent `think` is applied when the
73
+ # agent uses the native ollama driver.
74
74
  # =============================================================================
75
75
 
76
76
  config_version = "2.0"
@@ -154,6 +154,7 @@ driver = "ollama"
154
154
  base_url = "http://localhost:11434" # route :cloud models through the LOCAL daemon (auth via ~/.ollama/id_ed25519, one-time `ollama signin`)
155
155
  model = "deepseek-v4-pro:cloud" # orchestrator default; per-agent `model` overrides this
156
156
  num_ctx = 131072
157
+ think = true # default; set false for known non-thinking models
157
158
  # To hit ollama.com directly instead, swap the line above for:
158
159
  # base_url = "https://ollama.com" # no /v1; then `export OLLAMA_API_KEY=<key>` (the api_key field is inert on this path)
159
160
 
@@ -162,6 +163,7 @@ driver = "ollama"
162
163
  base_url = "http://localhost:11434"
163
164
  model = "qwen3.6:35b-a3b-coding-nvfp4"
164
165
  num_ctx = 32768 # keep modest: local VRAM is the constraint
166
+ think = true # default; set false if the selected model rejects thinking
165
167
 
166
168
  # Budget/throughput alternative — swap any cloud agent's `provider` to this, or
167
169
  # point a duplicate reviewer at it for cheap parallel passes.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ayder-cli"
7
- version = "2.2.7"
7
+ version = "2.2.8"
8
8
  description = "AI agent for any LLMs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -2,7 +2,7 @@ import logging
2
2
  import tomllib
3
3
  from pathlib import Path
4
4
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
5
- from typing import Any, Callable, Dict
5
+ from typing import Any, Callable, Dict, Literal
6
6
 
7
7
  logger = logging.getLogger(__name__)
8
8
 
@@ -319,6 +319,10 @@ class Config(BaseModel):
319
319
  max_history_messages: int = Field(default=0)
320
320
  prompt: str = Field(default="STANDARD")
321
321
  chat_protocol: str = Field(default="ollama")
322
+ # Ollama thinking mode. Defaults on because most local models in the
323
+ # primary path support it; set ``think = false`` in an [llm.*] profile for
324
+ # models that do not. Ollama also accepts "low", "medium", and "high".
325
+ think: bool | Literal["low", "medium", "high"] | None = Field(default=True)
322
326
  stop_sequences: list[str] = Field(default_factory=list)
323
327
  tool_tags: list[str] = Field(default_factory=lambda: ["core", "metadata"])
324
328
  temporal: TemporalConfig = Field(default_factory=TemporalConfig)
@@ -459,6 +463,25 @@ class Config(BaseModel):
459
463
  raise ValueError("max_history_messages must be non-negative (0 = unlimited)")
460
464
  return v
461
465
 
466
+ @field_validator("think", mode="before")
467
+ @classmethod
468
+ def validate_think(cls, v: Any) -> bool | Literal["low", "medium", "high"] | None:
469
+ if v is None or isinstance(v, bool):
470
+ return v
471
+ if isinstance(v, str):
472
+ normalized = v.strip().lower()
473
+ if normalized in {"true", "yes", "on", "1"}:
474
+ return True
475
+ if normalized in {"false", "no", "off", "0"}:
476
+ return False
477
+ if normalized == "low":
478
+ return "low"
479
+ if normalized == "medium":
480
+ return "medium"
481
+ if normalized == "high":
482
+ return "high"
483
+ raise ValueError("think must be true, false, low, medium, high, or null")
484
+
462
485
  @field_validator("agent_timeout")
463
486
  @classmethod
464
487
  def validate_agent_timeout(cls, v: int) -> int:
@@ -1,7 +1,7 @@
1
1
  """Ollama provider routed through per-family chat drivers."""
2
2
 
3
3
  import json
4
- from typing import Any, AsyncGenerator, Dict, List, Optional
4
+ from typing import Any, AsyncGenerator, Dict, List, Literal, Optional
5
5
 
6
6
  from loguru import logger
7
7
  from ollama import AsyncClient
@@ -19,6 +19,8 @@ from ayder_cli.providers.impl.ollama_drivers.base import ChatDriver, DriverMode
19
19
  from ayder_cli.providers.impl.ollama_drivers.registry import DriverRegistry
20
20
  from ayder_cli.providers.impl.ollama_inspector import OllamaInspector
21
21
 
22
+ ThinkOption = bool | Literal["low", "medium", "high"] | None
23
+
22
24
 
23
25
  class OllamaProvider(AIProvider):
24
26
  """Native wire-protocol provider for local Ollama models.
@@ -116,6 +118,50 @@ class OllamaProvider(AIProvider):
116
118
  ):
117
119
  yield chunk
118
120
 
121
+ def _configured_think(self) -> ThinkOption:
122
+ """Return the configured Ollama think option.
123
+
124
+ ``Config`` defaults this to True. Tests often pass loose mocks without a
125
+ real ``think`` field; those should behave like the real default.
126
+ """
127
+ value = getattr(self.config, "think", True)
128
+ if value is None or isinstance(value, bool):
129
+ return value
130
+ if isinstance(value, str):
131
+ normalized = value.strip().lower()
132
+ if normalized in {"true", "yes", "on", "1"}:
133
+ return True
134
+ if normalized in {"false", "no", "off", "0"}:
135
+ return False
136
+ if normalized == "low":
137
+ return "low"
138
+ if normalized == "medium":
139
+ return "medium"
140
+ if normalized == "high":
141
+ return "high"
142
+ return True
143
+
144
+ @staticmethod
145
+ def _is_unsupported_thinking_error(exc: BaseException) -> bool:
146
+ """Return True when Ollama rejected the requested thinking mode."""
147
+ message = str(exc).lower()
148
+ if "think" not in message and "thinking" not in message:
149
+ return False
150
+ return any(
151
+ marker in message
152
+ for marker in (
153
+ "not support",
154
+ "unsupported",
155
+ "invalid",
156
+ "unknown field",
157
+ "unrecognized",
158
+ )
159
+ )
160
+
161
+ @staticmethod
162
+ def _chunk_committed(chunk: NormalizedStreamChunk) -> bool:
163
+ return bool(chunk.content or chunk.reasoning or chunk.tool_calls)
164
+
119
165
  async def _stream_with_driver(
120
166
  self,
121
167
  driver: ChatDriver,
@@ -145,6 +191,34 @@ class OllamaProvider(AIProvider):
145
191
  model: str,
146
192
  tools: list[dict[str, Any]] | None,
147
193
  options: dict[str, Any] | None,
194
+ ) -> AsyncGenerator[NormalizedStreamChunk, None]:
195
+ think = self._configured_think()
196
+ committed = False
197
+ try:
198
+ async for chunk in self._stream_native_once(
199
+ messages, model, tools, options, think
200
+ ):
201
+ if self._chunk_committed(chunk):
202
+ committed = True
203
+ yield chunk
204
+ except BaseException as exc:
205
+ if committed or think is False or not self._is_unsupported_thinking_error(exc):
206
+ raise
207
+ logger.info(
208
+ f"Ollama model {model!r} rejected think={think!r}; retrying with think=False"
209
+ )
210
+ async for chunk in self._stream_native_once(
211
+ messages, model, tools, options, False
212
+ ):
213
+ yield chunk
214
+
215
+ async def _stream_native_once(
216
+ self,
217
+ messages: list[dict[str, Any]],
218
+ model: str,
219
+ tools: list[dict[str, Any]] | None,
220
+ options: dict[str, Any] | None,
221
+ think: ThinkOption,
148
222
  ) -> AsyncGenerator[NormalizedStreamChunk, None]:
149
223
  # Let Ollama size the context window from the model itself (recent
150
224
  # versions do this automatically); do not force num_ctx.
@@ -156,7 +230,7 @@ class OllamaProvider(AIProvider):
156
230
  messages=ollama_messages,
157
231
  tools=ollama_tools,
158
232
  keep_alive=-1,
159
- think=True,
233
+ think=think,
160
234
  stream=True,
161
235
  )
162
236
 
@@ -202,6 +276,35 @@ class OllamaProvider(AIProvider):
202
276
  model: str,
203
277
  tools: list[dict[str, Any]] | None,
204
278
  options: dict[str, Any] | None,
279
+ ) -> AsyncGenerator[NormalizedStreamChunk, None]:
280
+ think = self._configured_think()
281
+ committed = False
282
+ try:
283
+ async for chunk in self._stream_in_content_once(
284
+ driver, messages, model, tools, options, think
285
+ ):
286
+ if self._chunk_committed(chunk):
287
+ committed = True
288
+ yield chunk
289
+ except BaseException as exc:
290
+ if committed or think is False or not self._is_unsupported_thinking_error(exc):
291
+ raise
292
+ logger.info(
293
+ f"Ollama model {model!r} rejected think={think!r}; retrying with think=False"
294
+ )
295
+ async for chunk in self._stream_in_content_once(
296
+ driver, messages, model, tools, options, False
297
+ ):
298
+ yield chunk
299
+
300
+ async def _stream_in_content_once(
301
+ self,
302
+ driver: ChatDriver,
303
+ messages: list[dict[str, Any]],
304
+ model: str,
305
+ tools: list[dict[str, Any]] | None,
306
+ options: dict[str, Any] | None,
307
+ think: ThinkOption,
205
308
  ) -> AsyncGenerator[NormalizedStreamChunk, None]:
206
309
  # Let Ollama size the context window from the model itself; do not
207
310
  # force num_ctx.
@@ -213,7 +316,7 @@ class OllamaProvider(AIProvider):
213
316
  messages=ollama_messages,
214
317
  tools=None,
215
318
  keep_alive=-1,
216
- think=True,
319
+ think=think,
217
320
  stream=True,
218
321
  )
219
322
 
@@ -0,0 +1,12 @@
1
+ """Safe render helpers for Textual widgets."""
2
+
3
+ from textual.content import Content
4
+ from textual.markup import MarkupError
5
+
6
+
7
+ def markup_or_plain(content: str) -> Content:
8
+ """Render Textual markup, falling back to literal text if it is malformed."""
9
+ try:
10
+ return Content.from_markup(content)
11
+ except MarkupError:
12
+ return Content(content)
@@ -6,16 +6,17 @@ from typing import Any
6
6
 
7
7
  from textual.app import ComposeResult
8
8
  from textual.containers import Horizontal, VerticalScroll, Container
9
- from textual.widgets import Static, Input, Label, TextArea
10
- from textual.reactive import reactive
11
9
  from textual.message import Message
10
+ from textual.reactive import reactive
12
11
  from textual.suggester import SuggestFromList
12
+ from textual.widgets import Static, Input, Label, TextArea
13
13
  from rich.text import Text
14
14
  from rich.markdown import Markdown
15
15
  from rich.spinner import Spinner
16
16
  from rich.style import Style
17
17
 
18
18
  from ayder_cli.parser import content_processor
19
+ from ayder_cli.tui.rendering import markup_or_plain
19
20
  from ayder_cli.tui.types import MessageType
20
21
 
21
22
 
@@ -337,11 +338,12 @@ class ThinkingPanel(Container):
337
338
  self._buffer += text
338
339
 
339
340
  content = self._buffer.strip()
341
+ renderable = markup_or_plain(content)
340
342
  if self._widget is None:
341
- self._widget = Static(content, classes="thinking-content")
343
+ self._widget = Static(renderable, classes="thinking-content")
342
344
  self.mount(self._widget)
343
345
  else:
344
- self._widget.update(content)
346
+ self._widget.update(renderable)
345
347
  self.scroll_end(animate=False)
346
348
 
347
349
  def clear(self) -> None:
@@ -720,14 +722,14 @@ class _SubmitTextArea(TextArea):
720
722
  return
721
723
  if not self._file_picker_suggestions:
722
724
  self._file_picker_widget.display = False
723
- self._file_picker_widget.update("")
725
+ self._file_picker_widget.update(markup_or_plain(""))
724
726
  return
725
727
 
726
728
  lines = []
727
729
  for idx, suggestion in enumerate(self._file_picker_suggestions):
728
730
  marker = ">" if idx == self._file_picker_index else " "
729
731
  lines.append(f"{marker} @{suggestion}")
730
- self._file_picker_widget.update("\n".join(lines))
732
+ self._file_picker_widget.update(markup_or_plain("\n".join(lines)))
731
733
  self._file_picker_widget.display = True
732
734
 
733
735
  def _close_file_picker(self) -> None:
@@ -1108,8 +1110,8 @@ class StatusBar(Horizontal):
1108
1110
 
1109
1111
  def compose(self) -> ComposeResult:
1110
1112
  mode_str = "".join(sorted(self._permissions))
1111
- yield Label(f"model: {self.model}", id="model-label")
1112
- yield Label(f" | mode: {mode_str}", id="mode-label")
1113
+ yield Label(markup_or_plain(f"model: {self.model}"), id="model-label")
1114
+ yield Label(markup_or_plain(f" | mode: {mode_str}"), id="mode-label")
1113
1115
  yield Label(" | ctx: —", id="token-label")
1114
1116
  yield Label(" | files: 0", id="files-label")
1115
1117
  yield Label("", id="skill-label")
@@ -1126,7 +1128,7 @@ class StatusBar(Horizontal):
1126
1128
  """Update the displayed model."""
1127
1129
  self.model = model
1128
1130
  label = self.query_one("#model-label", Label)
1129
- label.update(f"model: {model}")
1131
+ label.update(markup_or_plain(f"model: {model}"))
1130
1132
 
1131
1133
  def update_context_usage(self, used: int, total: int) -> None:
1132
1134
  """Update the live context-window usage: current tokens / window size.
@@ -1151,12 +1153,12 @@ class StatusBar(Horizontal):
1151
1153
  self._permissions = permissions
1152
1154
  mode_str = "".join(sorted(permissions))
1153
1155
  label = self.query_one("#mode-label", Label)
1154
- label.update(f" | mode: {mode_str}")
1156
+ label.update(markup_or_plain(f" | mode: {mode_str}"))
1155
1157
 
1156
1158
  def set_skill(self, skill_name: str | None) -> None:
1157
1159
  """Update the active skill label."""
1158
1160
  label = self.query_one("#skill-label", Label)
1159
- label.update(f" | skill: {skill_name}" if skill_name else "")
1161
+ label.update(markup_or_plain(f" | skill: {skill_name}" if skill_name else ""))
1160
1162
 
1161
1163
  @staticmethod
1162
1164
  def _render_badges(
@@ -160,6 +160,39 @@ def test_max_concurrent_agents_range_validator():
160
160
  Config(max_concurrent_agents=bad)
161
161
 
162
162
 
163
+ def test_think_defaults_true():
164
+ assert Config().think is True
165
+
166
+
167
+ def test_think_accepts_false_and_levels():
168
+ assert Config(think=False).think is False
169
+ assert Config(think="false").think is False
170
+ assert Config(think="low").think == "low"
171
+ assert Config(think="medium").think == "medium"
172
+ assert Config(think="high").think == "high"
173
+
174
+
175
+ def test_think_rejects_invalid_value():
176
+ with pytest.raises(ValueError):
177
+ Config(think="extreme")
178
+
179
+
180
+ def test_think_can_be_set_in_llm_profile():
181
+ cfg = Config(
182
+ **{
183
+ "app": {"provider": "ollama_local"},
184
+ "llm": {
185
+ "ollama_local": {
186
+ "driver": "ollama",
187
+ "model": "gemma:latest",
188
+ "think": False,
189
+ }
190
+ },
191
+ }
192
+ )
193
+ assert cfg.think is False
194
+
195
+
163
196
  def test_provider_profile_infers_driver():
164
197
  # A [llm.<name>] profile with no explicit driver infers via _DRIVER_BY_PROVIDER.
165
198
  cfg = Config(**{"app": {"provider": "qwen"},
@@ -2,6 +2,7 @@
2
2
 
3
3
  from unittest.mock import AsyncMock, MagicMock, patch
4
4
 
5
+ from ollama import ResponseError
5
6
  import pytest
6
7
 
7
8
  from ayder_cli.providers.impl.ollama import OllamaProvider
@@ -70,6 +71,7 @@ async def test_provider_routes_through_drivers():
70
71
  pass
71
72
 
72
73
  assert captured_kwargs.get("tools") is not None
74
+ assert captured_kwargs.get("think") is True
73
75
  instance.show.assert_awaited_once_with("llama3.1:8b")
74
76
 
75
77
 
@@ -104,6 +106,7 @@ async def test_chat_protocol_xml_forces_generic_xml_driver():
104
106
  pass
105
107
 
106
108
  assert captured_kwargs.get("tools") is None
109
+ assert captured_kwargs.get("think") is True
107
110
  system_message = next(
108
111
  message for message in captured_kwargs["messages"] if message["role"] == "system"
109
112
  )
@@ -111,6 +114,112 @@ async def test_chat_protocol_xml_forces_generic_xml_driver():
111
114
  instance.show.assert_not_awaited()
112
115
 
113
116
 
117
+ @pytest.mark.asyncio
118
+ async def test_provider_sends_configured_think_false():
119
+ cfg = _config("llama3.1:8b")
120
+ cfg.think = False
121
+ captured_kwargs: dict = {}
122
+
123
+ async def fake_stream():
124
+ yield _mock_chunk(content="ok", done=True)
125
+
126
+ async def fake_chat(*args, **kwargs):
127
+ captured_kwargs.update(kwargs)
128
+ return fake_stream()
129
+
130
+ fake_show = MagicMock(
131
+ modelinfo={"llama.context_length": 32768},
132
+ capabilities=["tools"],
133
+ details=MagicMock(family="llama", quantization_level="Q4_K_M"),
134
+ )
135
+
136
+ with (
137
+ patch("ayder_cli.providers.impl.ollama.AsyncClient") as mock_client,
138
+ patch("ayder_cli.providers.impl.ollama_inspector.AsyncClient") as mock_inspector,
139
+ ):
140
+ instance = AsyncMock()
141
+ instance.chat.side_effect = fake_chat
142
+ instance.show = AsyncMock(return_value=fake_show)
143
+ mock_client.return_value = instance
144
+ mock_inspector.return_value = instance
145
+
146
+ provider = OllamaProvider(cfg)
147
+ async for _ in provider.stream_with_tools(
148
+ messages=[{"role": "user", "content": "hi"}],
149
+ model="llama3.1:8b",
150
+ tools=[{"type": "function", "function": {"name": "read_file"}}],
151
+ ):
152
+ pass
153
+
154
+ assert captured_kwargs.get("think") is False
155
+
156
+
157
+ @pytest.mark.asyncio
158
+ async def test_native_retries_with_think_false_when_model_rejects_thinking():
159
+ cfg = _config("llama3.1:8b")
160
+ cfg.think = True
161
+ calls: list[bool | str | None] = []
162
+
163
+ async def fake_stream():
164
+ yield _mock_chunk(content="ok", done=True)
165
+
166
+ async def fake_chat(*args, **kwargs):
167
+ calls.append(kwargs.get("think"))
168
+ if len(calls) == 1:
169
+ raise ResponseError("thinking is not supported by this model")
170
+ return fake_stream()
171
+
172
+ with patch("ayder_cli.providers.impl.ollama.AsyncClient") as mock_client:
173
+ instance = AsyncMock()
174
+ instance.chat.side_effect = fake_chat
175
+ mock_client.return_value = instance
176
+
177
+ provider = OllamaProvider(cfg)
178
+ results = [
179
+ chunk
180
+ async for chunk in provider._stream_native(
181
+ messages=[{"role": "user", "content": "hi"}],
182
+ model="llama3.1:8b",
183
+ tools=None,
184
+ options=None,
185
+ )
186
+ ]
187
+
188
+ assert calls == [True, False]
189
+ assert results[0].content == "ok"
190
+
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_native_does_not_retry_thinking_error_after_committed_output():
194
+ cfg = _config("llama3.1:8b")
195
+ calls: list[bool | str | None] = []
196
+
197
+ async def failing_stream():
198
+ yield _mock_chunk(content="partial")
199
+ raise ResponseError("thinking is not supported by this model")
200
+
201
+ async def fake_chat(*args, **kwargs):
202
+ calls.append(kwargs.get("think"))
203
+ return failing_stream()
204
+
205
+ with patch("ayder_cli.providers.impl.ollama.AsyncClient") as mock_client:
206
+ instance = AsyncMock()
207
+ instance.chat.side_effect = fake_chat
208
+ mock_client.return_value = instance
209
+
210
+ provider = OllamaProvider(cfg)
211
+ with pytest.raises(ResponseError):
212
+ async for _ in provider._stream_native(
213
+ messages=[{"role": "user", "content": "hi"}],
214
+ model="llama3.1:8b",
215
+ tools=None,
216
+ options=None,
217
+ ):
218
+ pass
219
+
220
+ assert calls == [True]
221
+
222
+
114
223
  @pytest.mark.asyncio
115
224
  async def test_driver_resolution_is_cached_across_turns():
116
225
  cfg = _config("llama3.1:8b")
@@ -15,6 +15,14 @@ def test_compose_yields_plugin_badges_label():
15
15
  assert "plugin-badges" in ids
16
16
 
17
17
 
18
+ def test_model_label_malformed_markup_falls_back_to_plain_text():
19
+ malformed = '[agent-result name="reviewer" run="3" status="completed"]'
20
+ bar = StatusBar(model=malformed)
21
+ labels = {getattr(w, "id", None): w for w in bar.compose()}
22
+
23
+ assert labels["model-label"].render().plain == f"model: {malformed}"
24
+
25
+
18
26
  def test_render_badges_empty_is_blank():
19
27
  rendered = StatusBar._render_badges({})
20
28
  assert rendered.plain == ""
@@ -2,7 +2,11 @@
2
2
 
3
3
  from unittest.mock import patch, MagicMock
4
4
 
5
- from ayder_cli.tui.widgets import ActivityBar, AgentPanel, ThinkingPanel
5
+ from textual.content import Content
6
+ from textual.markup import MarkupError
7
+ from textual.widgets import Static
8
+
9
+ from ayder_cli.tui.widgets import ActivityBar, AgentPanel, ThinkingPanel, _SubmitTextArea
6
10
 
7
11
 
8
12
  class TestActivityBarAgents:
@@ -82,6 +86,41 @@ class TestThinkingPanelStreaming:
82
86
  assert panel._widget is None
83
87
  mock_mount.assert_not_called()
84
88
 
89
+ @patch.object(ThinkingPanel, "scroll_end")
90
+ @patch.object(ThinkingPanel, "mount")
91
+ def test_malformed_markup_falls_back_to_plain_text(self, mock_mount, _mock_scroll):
92
+ panel = ThinkingPanel()
93
+ content = '[agent-result name="reviewer" run="3" status="completed"]'
94
+
95
+ try:
96
+ Content.from_markup(content)
97
+ except MarkupError:
98
+ pass
99
+ else:
100
+ raise AssertionError("test fixture should be malformed Textual markup")
101
+
102
+ panel.add_thinking(content)
103
+
104
+ assert panel._widget is not None
105
+ assert panel._widget.render().plain == content
106
+ mock_mount.assert_called_once()
107
+
108
+ @patch.object(ThinkingPanel, "scroll_end")
109
+ @patch.object(ThinkingPanel, "mount")
110
+ def test_streamed_malformed_markup_update_falls_back_to_plain_text(
111
+ self, mock_mount, _mock_scroll
112
+ ):
113
+ panel = ThinkingPanel()
114
+ malformed = '[calls call_agent: name="code-reviewer", task="Review src/auth.py"]'
115
+ expected = f"safe prefix {malformed}"
116
+
117
+ panel.add_thinking("safe prefix ")
118
+ panel.add_thinking(malformed)
119
+
120
+ assert panel._widget is not None
121
+ assert panel._widget.render().plain == expected
122
+ mock_mount.assert_called_once()
123
+
85
124
  @patch.object(ThinkingPanel, "scroll_end")
86
125
  @patch.object(ThinkingPanel, "mount")
87
126
  def test_start_phase_inserts_separator_between_phases(self, mock_mount, _mock_scroll):
@@ -111,6 +150,18 @@ class TestThinkingPanelStreaming:
111
150
  assert panel._needs_separator is False
112
151
 
113
152
 
153
+ class TestFilePickerRendering:
154
+ def test_malformed_markup_path_falls_back_to_plain_text(self):
155
+ input_widget = _SubmitTextArea()
156
+ input_widget._file_picker_widget = Static("", id="file-picker")
157
+ malformed = '[agent-result name="reviewer" run="3" status="completed"]'
158
+ input_widget._file_picker_suggestions = [malformed]
159
+
160
+ input_widget._render_file_picker()
161
+
162
+ assert input_widget._file_picker_widget.render().plain == f"> @{malformed}"
163
+
164
+
114
165
  class TestAgentPanelDataModel:
115
166
  @patch.object(AgentPanel, "mount")
116
167
  def test_add_agent_creates_entry(self, mock_mount):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes