mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 02:46:02 +00:00
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:
parent
4bf028d048
commit
8dafb667dd
@ -20,6 +20,7 @@ from textual.widgets.option_list import Option
|
|||||||
|
|
||||||
from deerflow.runtime.goal import parse_goal_command
|
from deerflow.runtime.goal import parse_goal_command
|
||||||
|
|
||||||
|
from .command_registry import format_command_help
|
||||||
from .input_history import InputHistory
|
from .input_history import InputHistory
|
||||||
from .render import render_header, render_status, render_transcript
|
from .render import render_header, render_status, render_transcript
|
||||||
from .runtime import stream_actions
|
from .runtime import stream_actions
|
||||||
@ -36,7 +37,8 @@ from .view_state import (
|
|||||||
)
|
)
|
||||||
from .widgets.composer import ComposerInput
|
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):
|
class SelectScreen(ModalScreen):
|
||||||
|
|||||||
@ -57,6 +57,17 @@ BUILTIN_COMMANDS: tuple[Command, ...] = (
|
|||||||
_BUILTIN_NAMES = frozenset(c.name for c in BUILTIN_COMMANDS)
|
_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]:
|
def build_registry(skills: list[dict]) -> list[Command]:
|
||||||
"""Merge built-ins with one command per enabled skill."""
|
"""Merge built-ins with one command per enabled skill."""
|
||||||
commands = list(BUILTIN_COMMANDS)
|
commands = list(BUILTIN_COMMANDS)
|
||||||
|
|||||||
@ -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" for r in app.state.rows)
|
||||||
assert any(r.kind == "system" and "/clear" in r.text 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.
|
# /help must not produce a user turn or start streaming.
|
||||||
assert not any(r.kind == "user" for r in app.state.rows)
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_clear_command_clears_display_without_resetting_thread_or_calling_agent():
|
async def test_clear_command_clears_display_without_resetting_thread_or_calling_agent():
|
||||||
session = _FakeSession()
|
session = _FakeSession()
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from deerflow.tui.command_registry import (
|
|||||||
BUILTIN_COMMANDS,
|
BUILTIN_COMMANDS,
|
||||||
build_registry,
|
build_registry,
|
||||||
filter_commands,
|
filter_commands,
|
||||||
|
format_command_help,
|
||||||
resolve,
|
resolve,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -115,3 +116,38 @@ def test_goal_builtin_takes_precedence_over_skill():
|
|||||||
|
|
||||||
assert [command.name for command in registry].count("goal") == 1
|
assert [command.name for command in registry].count("goal") == 1
|
||||||
assert resolve("/goal finish", skills=["goal"]).kind == "builtin"
|
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: /")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user