Skip to content

Commit fe583b6

Browse files
committed
fix: isolate slash command autocomplete to command names
1 parent e911835 commit fe583b6

File tree

4 files changed

+104
-4
lines changed

4 files changed

+104
-4
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: Slash-command autocomplete intercepted `/skills` argument navigation
3+
link: command-autocomplete-argument-interference
4+
type: bugfix
5+
path: src/tunacode/ui/widgets/command_autocomplete.py
6+
tags:
7+
- ui
8+
- autocomplete
9+
- commands
10+
- skills
11+
created_at: 2026-03-06T22:35:00-06:00
12+
updated_at: 2026-03-06T22:35:00-06:00
13+
---
14+
15+
## Summary
16+
17+
Fixed the root cause of broken `/skills` navigation: the generic slash-command autocomplete was still building candidates while the user was editing command arguments, so arrow and Enter keys could be consumed by the wrong dropdown.
18+
19+
## Trigger
20+
21+
Typing `/skills ...` correctly opened the skills dropdown, but pressing Down caused the hidden slash-command autocomplete to activate and move its own highlight. Enter could then complete `/cancel`, `/compact`, or another slash command instead of the highlighted skill.
22+
23+
## Changes
24+
25+
- Tightened `CommandAutoComplete.get_candidates()` to return candidates only while the user is editing the command-name region before the first space.
26+
- Reused the same command-name parser already used by visibility logic.
27+
- Added focused tests proving command autocomplete is inactive during `/skills` argument editing.
28+
- Added an end-to-end headless UI test proving `/skills ` + Down + Enter selects the highlighted skill, not a slash command.
29+
30+
## Tests
31+
32+
- `uv run pytest tests/unit/ui/test_command_autocomplete.py tests/unit/ui/test_skills_autocomplete.py -q`
33+
- `uv run ruff check src/tunacode/ui/widgets/command_autocomplete.py tests/unit/ui/test_command_autocomplete.py tests/unit/ui/test_skills_autocomplete.py`

src/tunacode/ui/widgets/command_autocomplete.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,15 @@ def should_show_dropdown(self, search_string: str) -> bool: # noqa: ARG002
7171
return not _is_exact_command_match(command_prefix)
7272

7373
def get_candidates(self, target_state: TargetState) -> list[DropdownItem]:
74-
"""Return command candidates for current search."""
75-
if not target_state.text.startswith(COMMAND_PREFIX):
74+
"""Return command candidates only while editing the slash-command name."""
75+
command_prefix = _get_command_search_prefix(
76+
target_state.text,
77+
target_state.cursor_position,
78+
)
79+
if command_prefix is None:
7680
return []
7781

78-
search = self.get_search_string(target_state).lower()
82+
search = command_prefix.lower()
7983
return [
8084
DropdownItem(main=f"{COMMAND_PREFIX}{name}{COMMAND_DESCRIPTION_SEPARATOR}{desc}")
8185
for name, desc in _COMMAND_ITEMS

tests/unit/ui/test_command_autocomplete.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
async def _type_text(pilot, text: str) -> None:
1313
for character in text:
14-
await pilot.press(character)
14+
key = "space" if character == " " else character
15+
await pilot.press(key)
16+
1517

1618

1719
@pytest.mark.parametrize("command_name", ["help", "compact"])
@@ -47,3 +49,17 @@ async def test_exact_match_enter_submits_slash_command() -> None:
4749
assert app.editor.value == ""
4850
assert len(app.chat_container.children) == initial_message_count + 1
4951
assert isinstance(app.chat_container.children[-1].renderable, Table)
52+
53+
54+
async def test_command_autocomplete_ignores_slash_command_arguments() -> None:
55+
app = TextualReplApp(state_manager=StateManager())
56+
57+
async with app.run_test(headless=True) as pilot:
58+
autocomplete = app.query_one(CommandAutoComplete)
59+
60+
await _type_text(pilot, "/skills de")
61+
await pilot.pause()
62+
63+
assert app.editor.value == "/skills de"
64+
assert autocomplete.option_list.option_count == 0
65+
assert autocomplete.display is False

tests/unit/ui/test_skills_autocomplete.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from tunacode.core.session import StateManager
88

99
from tunacode.ui.app import TextualReplApp
10+
from tunacode.ui.widgets.command_autocomplete import CommandAutoComplete
1011
from tunacode.ui.widgets.skills_autocomplete import SkillsAutoComplete
1112

1213
SKILL_TEMPLATE = """---
@@ -136,6 +137,52 @@ async def test_skills_autocomplete_enter_prefers_best_prefix_match(
136137
assert app.state_manager.session.selected_skill_names == ["demo"]
137138

138139

140+
async def test_skills_autocomplete_down_selects_highlighted_skill(
141+
tmp_path: Path,
142+
monkeypatch,
143+
) -> None:
144+
monkeypatch.chdir(tmp_path)
145+
monkeypatch.setenv("HOME", str(tmp_path / "home"))
146+
_write_skill(tmp_path, "alpha", description="Alpha skill")
147+
_write_skill(tmp_path, "beta", description="Beta skill")
148+
149+
app = TextualReplApp(state_manager=StateManager())
150+
151+
async with app.run_test(headless=True) as pilot:
152+
command_autocomplete = app.query_one(CommandAutoComplete)
153+
skills_autocomplete = app.query_one(SkillsAutoComplete)
154+
155+
await _type_text(pilot, "/skills ")
156+
await pilot.pause()
157+
158+
assert app.editor.value == "/skills "
159+
assert command_autocomplete.option_list.option_count == 0
160+
assert command_autocomplete.display is False
161+
assert skills_autocomplete.display is True
162+
163+
await pilot.press("down")
164+
await pilot.press("down")
165+
await pilot.press("down")
166+
await pilot.pause()
167+
168+
highlighted_index = skills_autocomplete.option_list.highlighted
169+
assert highlighted_index == 3
170+
highlighted_option = skills_autocomplete.option_list.get_option_at_index(highlighted_index)
171+
assert highlighted_option.value == "alpha"
172+
173+
await pilot.press("enter")
174+
await pilot.pause()
175+
176+
assert app.editor.value == "/skills alpha"
177+
assert command_autocomplete.display is False
178+
179+
await pilot.press("enter")
180+
await pilot.pause()
181+
182+
assert app.editor.value == ""
183+
assert app.state_manager.session.selected_skill_names == ["alpha"]
184+
185+
139186
async def _type_text(pilot, text: str) -> None:
140187
for character in text:
141188
key = "space" if character == " " else character

0 commit comments

Comments
 (0)