feat(cli): autocomplete + ghost text for stacked slash-skill invocations (#58763)

Follow-up to #57987: after /skill-a the completer previously went silent
for a second /skill token. Now, while the leading tokens form an unbroken
skill chain (each token a distinct installed skill, under the 5-cap) and
the word under the cursor starts with '/', the completer keeps offering
the remaining skill commands, and SlashCommandAutoSuggest ghost-suggests
the rest of the next skill name. Instruction text, path-like tokens, and
broken chains get no suggestions. The TUI's complete.slash RPC reuses
SlashCommandCompleter, so it inherits the behavior with no changes.
This commit is contained in:
Teknium 2026-07-05 04:34:14 -07:00 committed by GitHub
parent 1c156736dc
commit 2c0820c9ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 184 additions and 0 deletions

View file

@ -1355,6 +1355,77 @@ class SlashCommandCompleter(Completer):
except Exception:
return {}
# -- stacked slash-skill completion helpers ---------------------------
@staticmethod
def _normalize_skill_token(token: str) -> str:
"""Canonicalize a typed skill token to its hyphenated /slug form.
Mirrors resolve_skill_command_key() in agent/skill_commands.py:
underscores (Telegram bot-command form) are interchangeable with
hyphens.
"""
return "/" + token.lstrip("/").replace("_", "-").lower()
def _is_skill_command(self, token: str) -> bool:
return self._normalize_skill_token(token) in self._iter_skill_commands()
def _stacked_skill_completions(self, text: str):
"""Offer skill-command completions for stacked invocations.
After ``/skill-a `` the user may chain more leading skills
(``/skill-a /skill-b do XYZ``). While every whitespace-delimited
token so far resolves to a distinct skill command and the current
word under the cursor starts with ``/``, keep offering the remaining
skill commands. The moment the chain is broken (a non-skill token
appears, the cap is reached, or the user is typing plain instruction
text) we offer nothing instruction text must never be polluted
with skill suggestions.
"""
try:
from agent.skill_commands import _MAX_STACKED_SKILLS as _cap
except Exception:
_cap = 5
tokens = text.split()
if text.endswith(" "):
completed, current_word = tokens, ""
else:
completed, current_word = tokens[:-1], tokens[-1]
# The chain must be unbroken: every completed token is a distinct
# skill command, and there's room left under the cap.
seen: set[str] = set()
for token in completed:
key = self._normalize_skill_token(token)
if key not in self._iter_skill_commands() or key in seen:
return
seen.add(key)
if len(seen) >= _cap:
return
# Only suggest while the user is typing another /token — a bare
# space after the chain means they may be starting the instruction.
if not current_word.startswith("/"):
return
word_key = self._normalize_skill_token(current_word)
for cmd, info in self._iter_skill_commands().items():
if cmd in seen or not cmd.startswith(word_key):
continue
description = str(info.get("description", "Skill command"))
short_desc = description[:50] + ("..." if len(description) > 50 else "")
# Exact match: append a trailing space so the dropdown stays
# visible and the next stacked token can be typed immediately
# (mirrors _completion_text semantics).
replacement = f"{cmd} " if cmd == word_key else cmd
yield Completion(
replacement,
start_position=-len(current_word),
display=cmd,
display_meta=f"{short_desc}",
)
# Commands that open pickers when run without arguments.
# These should NOT receive a trailing space in completions because:
# - The TUI's submit handler applies completions on Enter if input differs
@ -1889,6 +1960,15 @@ class SlashCommandCompleter(Completer):
sub_text = parts[1] if len(parts) > 1 else ""
sub_lower = sub_text.lower()
# Stacked slash-skill invocations: after `/skill-a ` the user may
# chain more skills (`/skill-a /skill-b …`), so keep offering
# skill-command completions while the leading-skill chain is
# unbroken (see split_stacked_skill_commands in
# agent/skill_commands.py).
if self._is_skill_command(base_cmd):
yield from self._stacked_skill_completions(text)
return
# Dynamic completions for commands with runtime lists
if " " not in sub_text:
if base_cmd == "/skin":
@ -2023,6 +2103,20 @@ class SlashCommandAutoSuggest(AutoSuggest):
sub_text = parts[1] if len(parts) > 1 else ""
sub_lower = sub_text.lower()
# Stacked slash-skill invocations: while the leading tokens form an
# unbroken skill chain and the user is typing another /token,
# ghost-suggest the rest of the next skill name. Otherwise fall
# through to the history fallback for instruction text.
if (
self._completer is not None
and self._completer._is_skill_command(base_cmd)
):
for completion in self._completer._stacked_skill_completions(text):
remainder = completion.text[-completion.start_position:] \
if completion.start_position else completion.text
if remainder.strip():
return Suggestion(remainder)
# Static subcommands
if self._completer is not None and not self._completer._command_allowed(base_cmd):
return None