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.
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/PKG-INFO +1 -1
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/config.toml.example +14 -12
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/pyproject.toml +1 -1
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/config.py +24 -1
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama.py +106 -3
- ayder_cli-2.2.8/src/ayder_cli/tui/rendering.py +12 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/widgets.py +13 -11
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_v2.py +33 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_provider_integration.py +109 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_status_bar_badges.py +8 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_widgets.py +52 -1
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/.github/workflows/ci.yml +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/.github/workflows/python-package.yml +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/.gitignore +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/.python-version +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/AGENTS.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/LICENSE +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/README.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/PROJECT_STRUCTURE.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/README_AGENT_WORKFLOW.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/README_OLLAMA.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/cc.png +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/superpowers/plans/2026-06-26-file-editor-improvements.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/superpowers/plans/2026-06-26-session-save-resume.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/superpowers/specs/2026-06-26-file-editor-improvements-design.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/superpowers/specs/2026-06-26-session-save-resume-design.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/__main__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/callbacks.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/config.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/registry.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/run.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/runner.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/agents/worktree.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/README.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/execution_policy.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/message_contract.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/runtime_factory.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/application/validation.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/cli.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/cli_callbacks.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/cli_runner.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/console.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/cache_monitor.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/config_migration.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/context.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/context_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/context_manager_factory.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/default_context_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/ollama_context_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/result.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/core/session.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/logging_config.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/loops/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/loops/chat_loop.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/parser.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/process_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/prompts.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/base.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/claude.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/deepseek.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/gemini.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/glm.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/_errors.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/base.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/deepseek.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/generic_native.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/generic_xml.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/matrix.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/minimax.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/qwen3.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_drivers/registry.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/ollama_inspector.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/openai.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/impl/qwen.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/orchestrator.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/providers/retry.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/services/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/services/interactions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/themes/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/themes/ayder.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/themes/claude.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/background_process_tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/context.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/context_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/filesystem.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/filesystem_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/note_tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/notes.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/notes_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/process_manager_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/search.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/search_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/shell.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/shell_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/skill.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/skill_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/task_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/task_tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/tasks.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/utils_tools.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/utils_tools_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/web.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/builtins/web_definitions.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/definition.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/execution.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/hooks.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/normalization.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/plugin_api.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/plugin_github.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/plugin_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/plugin_status.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/registry.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/schemas.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tools/utils.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/adapter.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/app.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/commands.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/helpers.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/keybindings.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/parser.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/screens.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/theme_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/tui/types.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/ui.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/src/ayder_cli/version.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/COVERAGE.md +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_agent_consolidated.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_agent_tools.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_callbacks.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_config.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_integration.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_queue.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_registry.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_run.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_runner.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_runner_provider_unavailable.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_worktree_helpers.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/agents/test_worktree_isolation.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/application/test_message_contract.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/application/test_runtime_factory.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/client/test_main.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/convergence/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/convergence/test_execution_policy_parity.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/convergence/test_runtime_wiring.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/convergence/test_validation_path.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_cache_monitor.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_coverage.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_migration.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_provider_logging.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_config_retry.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_context_manager_factory.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_default_context_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_default_context_manager_should_compact.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_ollama_context_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_ollama_context_manager_cache_monitor.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_ollama_context_manager_max_history.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_parameter_aliasing.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_parser.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/core/test_session.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/loops/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/loops/test_base.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/loops/test_chat_loop_hook.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/loops/test_chat_loop_tool_truncation.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_base.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_deepseek.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_errors.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_fallback.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_generic_native.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_generic_xml.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_matrix.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_minimax.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_qwen3.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_registry.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_glm_async.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_inspector.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_native.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_xml.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_xml_autoroute.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_ollama_xml_usage.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_orchestrator_capabilities.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_qwen_async.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_retry_classifier.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_retry_integration.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/test_retrying_provider.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/services/test_boundary.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/services/test_interaction_sink.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_cli.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_cli_callbacks.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_context.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_env_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_logging_config.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_notes.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_process_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/test_tasks.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_agent_notes.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_background_process_tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_bash_tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_definition_discovery.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_di_injection.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_impl.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_impl_coverage.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_note_tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_notes_api.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_path_security.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_api.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_github.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_loading.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_manager.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_plugin_status.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_registry.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_registry_coverage.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_result.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_schemas.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_search_codebase.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_skill_tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_task_tool.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_utils.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tools/test_web.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/__init__.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_agent_list_screen.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_do_clear_coordination.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_follow_mode.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_help_screen.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_input_box.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_keybindings.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_multi_select_screen.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_multiline_input.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_paste.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_pending_compact_consumer.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_pull_delivery.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_resume.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_themes.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/tui/test_turn_consumer.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_confirm_screen.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_diff_preview.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_plugin_autoenable.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_provider_switch.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_run_tui_startup.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_assistant_sanitization.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_chat_loop.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_agent.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_logging.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_plugin.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_provider.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_commands_skill.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_helpers.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_temporal_command.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_tui_widgets.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_ui.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_ui_coverage.py +0 -0
- {ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/ui/test_ui_verbose.py +0 -0
|
@@ -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
|
|
52
|
-
# (think=
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
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 —
|
|
64
|
-
# sends
|
|
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
|
|
70
|
-
#
|
|
71
|
-
#
|
|
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.
|
|
@@ -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=
|
|
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=
|
|
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(
|
|
343
|
+
self._widget = Static(renderable, classes="thinking-content")
|
|
342
344
|
self.mount(self._widget)
|
|
343
345
|
else:
|
|
344
|
-
self._widget.update(
|
|
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"},
|
{ayder_cli-2.2.7 → ayder_cli-2.2.8}/tests/providers/ollama_drivers/test_provider_integration.py
RENAMED
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ayder_cli-2.2.7 → ayder_cli-2.2.8}/docs/superpowers/plans/2026-06-26-file-editor-improvements.md
RENAMED
|
File without changes
|