fix(tui): derive /help text from the command registry (#4327)

The /help string was hardcoded and had drifted from BUILTIN_COMMANDS,
omitting six real commands (help, switch, resume, uploads, artifacts,
details) and ordering the rest differently from the picker. Generate the
command line from the registry so /help can never fall out of sync again,
and add parity tests that guard against future drift.
This commit is contained in:
Ryker_Feng 2026-07-22 07:53:50 +08:00 committed by GitHub
parent 4bf028d048
commit 8dafb667dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 71 additions and 1 deletions

View File

@ -20,6 +20,7 @@ from textual.widgets.option_list import Option
from deerflow.runtime.goal import parse_goal_command
from .command_registry import format_command_help
from .input_history import InputHistory
from .render import render_header, render_status, render_transcript
from .runtime import stream_actions
@ -36,7 +37,8 @@ from .view_state import (
)
from .widgets.composer import ComposerInput
_HELP_TEXT = "Commands: /new /clear /threads /goal /model /skills /tools /mcp /memory /usage /config /quit\nKeys: Enter send · Ctrl+C interrupt or quit · Ctrl+L redraw · / commands · Esc close overlay"
_HELP_KEYS = "Keys: Enter send · Ctrl+C interrupt or quit · Ctrl+L redraw · / commands · Esc close overlay"
_HELP_TEXT = f"{format_command_help()}\n{_HELP_KEYS}"
class SelectScreen(ModalScreen):

View File

@ -57,6 +57,17 @@ BUILTIN_COMMANDS: tuple[Command, ...] = (
_BUILTIN_NAMES = frozenset(c.name for c in BUILTIN_COMMANDS)
def format_command_help() -> str:
"""One-line summary of every built-in slash command, for ``/help``.
Derived from :data:`BUILTIN_COMMANDS` so the help text can never drift out
of sync with the registry (and, therefore, the picker). Adding a built-in
surfaces it in ``/help`` automatically.
"""
names = " ".join(f"/{command.name}" for command in BUILTIN_COMMANDS)
return f"Commands: {names}"
def build_registry(skills: list[dict]) -> list[Command]:
"""Merge built-ins with one command per enabled skill."""
commands = list(BUILTIN_COMMANDS)

View File

@ -95,10 +95,31 @@ async def test_help_command_renders_system_row_without_calling_agent():
assert any(r.kind == "system" for r in app.state.rows)
assert any(r.kind == "system" and "/clear" in r.text for r in app.state.rows)
# Commands previously missing from the hardcoded help string must now appear,
# since the help text is derived from the command registry.
help_rows = [r for r in app.state.rows if r.kind == "system"]
for command in ("/help", "/resume", "/switch", "/uploads", "/artifacts", "/details"):
assert any(command in r.text for r in help_rows), command
# /help must not produce a user turn or start streaming.
assert not any(r.kind == "user" for r in app.state.rows)
@pytest.mark.asyncio
async def test_help_text_matches_command_registry():
from deerflow.tui.command_registry import format_command_help
app = DeerFlowTUI(_FakeSession(), LaunchPlan(mode="tui"))
async with app.run_test() as pilot:
await pilot.pause()
for ch in "/help":
await pilot.press(ch)
await pilot.press("enter")
await _wait_until(lambda: any(r.kind == "system" for r in app.state.rows), pilot)
expected = format_command_help()
assert any(r.kind == "system" and expected in r.text for r in app.state.rows)
@pytest.mark.asyncio
async def test_clear_command_clears_display_without_resetting_thread_or_calling_agent():
session = _FakeSession()

View File

@ -4,6 +4,7 @@ from deerflow.tui.command_registry import (
BUILTIN_COMMANDS,
build_registry,
filter_commands,
format_command_help,
resolve,
)
@ -115,3 +116,38 @@ def test_goal_builtin_takes_precedence_over_skill():
assert [command.name for command in registry].count("goal") == 1
assert resolve("/goal finish", skills=["goal"]).kind == "builtin"
# --------------------------------------------------------------------------- #
# /help text <-> registry parity
# --------------------------------------------------------------------------- #
def test_help_lists_every_builtin_command():
help_line = format_command_help()
# Every registered built-in must be advertised in /help; this is the guard
# against the help text silently drifting from BUILTIN_COMMANDS.
for command in BUILTIN_COMMANDS:
assert f"/{command.name}" in help_line
def test_help_lists_only_builtin_commands():
help_line = format_command_help()
slugs = [token[1:] for token in help_line.split() if token.startswith("/")]
assert set(slugs) == {command.name for command in BUILTIN_COMMANDS}
def test_help_preserves_registry_order():
help_line = format_command_help()
slugs = [token[1:] for token in help_line.split() if token.startswith("/")]
assert slugs == [command.name for command in BUILTIN_COMMANDS]
def test_help_has_no_duplicate_commands():
help_line = format_command_help()
slugs = [token[1:] for token in help_line.split() if token.startswith("/")]
assert len(slugs) == len(set(slugs))
def test_help_starts_with_commands_label():
assert format_command_help().startswith("Commands: /")