Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai>
Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai>
Co-Authored-By: Clément Drouin <clement.drouin@mistral.ai>
Co-Authored-By: Vincent Guilloux <vincent.guilloux@mistral.ai>
Co-Authored-By: Clément Siriex <clement.sirieix@mistral.ai>
Co-Authored-By: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-Authored-By: Thaddee Tyl <thaddee.tyl@gmail.com>
Co-Authored-By: David Brochart <david.brochart@gmail.com>
Co-Authored-By: Joseph Guhlin <joseph.guhlin@gmail.com>
Co-Authored-By: Thomas Kenbeek <thomaskenbeek@gmail.com>
Co-Authored-By: Remenby31 <baptiste.cruvellier31@gmail.com>
This commit is contained in:
Mathias Gesbert 2026-01-27 16:39:30 +01:00 committed by Mathias Gesbert
parent 79f215d91c
commit d33db9fff8
217 changed files with 16911 additions and 4305 deletions

View file

@ -61,7 +61,7 @@ def make_controller(
("/exit", "Exit application"),
("/vim", "Toggle vim keybindings"),
]
completer = CommandCompleter(commands)
completer = CommandCompleter(lambda: commands)
view = StubView()
controller = SlashCommandController(completer, view)
@ -162,3 +162,74 @@ def test_on_key_enter_submits_selected_completion() -> None:
assert result is CompletionResult.SUBMIT
assert view.replacements == [Replacement(0, 2, "/compact")]
assert view.reset_count == 1
def test_callable_entries_updates_completions_dynamically() -> None:
"""Test that CommandCompleter with a callable updates entries when the callable returns different values.
This simulates config reload where available skills change.
"""
available_skills: list[tuple[str, str]] = []
def get_entries() -> list[tuple[str, str]]:
base_commands = [("/help", "Display help"), ("/config", "Show configuration")]
return base_commands + available_skills
completer = CommandCompleter(get_entries)
view = StubView()
controller = SlashCommandController(completer, view)
# Initially, only base commands are available
controller.on_text_changed("/", cursor_index=1)
suggestions, _ = view.suggestion_events[-1]
assert [s.alias for s in suggestions] == ["/help", "/config"]
# Simulate config reload: add a skill
available_skills.append(("/summarize", "Summarize the conversation"))
# Now completions should include the new skill
controller.on_text_changed("/", cursor_index=1)
suggestions, _ = view.suggestion_events[-1]
assert [s.alias for s in suggestions] == ["/help", "/config", "/summarize"]
# And searching for "/s" should find the new skill
controller.on_text_changed("/s", cursor_index=2)
suggestions, _ = view.suggestion_events[-1]
assert [s.alias for s in suggestions] == ["/summarize"]
assert suggestions[0].description == "Summarize the conversation"
def test_callable_entries_reflects_enabled_disabled_skills() -> None:
"""Test that skill enable/disable changes are reflected in completions.
This simulates the scenario where a user changes enabled_skills in config
and runs /reload.
"""
enabled_skills: set[str] = {"commit", "review"}
all_skills = [
("/commit", "Create a git commit"),
("/review", "Review code changes"),
("/deploy", "Deploy to production"),
]
def get_entries() -> list[tuple[str, str]]:
return [(name, desc) for name, desc in all_skills if name[1:] in enabled_skills]
completer = CommandCompleter(get_entries)
view = StubView()
controller = SlashCommandController(completer, view)
# Initially only commit and review are enabled
controller.on_text_changed("/", cursor_index=1)
suggestions, _ = view.suggestion_events[-1]
assert [s.alias for s in suggestions] == ["/commit", "/review"]
# Simulate config reload: enable deploy, disable commit
enabled_skills.discard("commit")
enabled_skills.add("deploy")
# Now completions should reflect the change
controller.on_text_changed("/", cursor_index=1)
suggestions, _ = view.suggestion_events[-1]
assert [s.alias for s in suggestions] == ["/review", "/deploy"]

View file

@ -10,6 +10,7 @@ from textual.widgets import Markdown
from vibe.cli.textual_ui.app import VibeApp
from vibe.cli.textual_ui.widgets.chat_input.completion_popup import CompletionPopup
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
from vibe.core.agent_loop import AgentLoop
from vibe.core.config import SessionLoggingConfig, VibeConfig
@ -20,7 +21,8 @@ def vibe_config() -> VibeConfig:
@pytest.fixture
def vibe_app(vibe_config: VibeConfig) -> VibeApp:
return VibeApp(config=vibe_config)
agent_loop = AgentLoop(vibe_config)
return VibeApp(agent_loop=agent_loop)
@pytest.mark.asyncio
@ -60,7 +62,7 @@ async def test_pressing_tab_writes_selected_command_and_keeps_popup_visible(
await pilot.press(*"/co")
await pilot.press("tab")
assert chat_input.value == "/config"
assert chat_input.value == "/compact"
assert popup.styles.display == "block"
@ -88,11 +90,11 @@ async def test_arrow_navigation_updates_selected_suggestion(vibe_app: VibeApp) -
await pilot.press(*"/c")
ensure_selected_command(popup, "/config")
await pilot.press("down")
ensure_selected_command(popup, "/clear")
await pilot.press("down")
ensure_selected_command(popup, "/compact")
await pilot.press("up")
ensure_selected_command(popup, "/config")
ensure_selected_command(popup, "/clear")
@pytest.mark.asyncio
@ -102,11 +104,11 @@ async def test_arrow_navigation_cycles_through_suggestions(vibe_app: VibeApp) ->
await pilot.press(*"/co")
ensure_selected_command(popup, "/config")
await pilot.press("down")
ensure_selected_command(popup, "/compact")
await pilot.press("up")
await pilot.press("down")
ensure_selected_command(popup, "/config")
await pilot.press("up")
ensure_selected_command(popup, "/compact")
@pytest.mark.asyncio