Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/bach/repl/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ def run(self) -> None:
# correctly suggests `/project-add` / `/project-init` / `/projects`.
# Returns None when prompt_toolkit isn't installed — completer
# kwarg is then silently ignored downstream.
main_completer = _build_word_completer(sorted(_HANDLERS.keys()), sentence=True)
# Offer both spellings in tab-completion: canonical /cmd and bare cmd.
completion_words = sorted({*_HANDLERS.keys(), *(k.lstrip("/") for k in _HANDLERS)})
main_completer = _build_word_completer(completion_words, sentence=True)

while not self._should_quit:
_render_dashboard(self)
Expand Down Expand Up @@ -469,14 +471,24 @@ def _format_title_with_hierarchy(task: DailyTask, theme: ThemePalette) -> str:


def _dispatch(app: BachRepl, line: str) -> None:
"""Parse, resolve aliases, run the handler."""
"""Parse, resolve aliases, run the handler.

Commands work with or without the leading slash: `sessions bach` is
`/sessions bach`. The slashless form is resolved by retrying the
lookup with a '/' prefix, so '/' stays the canonical spelling and the
handler table never needs duplicate keys.
"""
name, *args = line.split(maxsplit=1)
arg_str = args[0] if args else ""
canonical = _ALIASES.get(name, name)
handler = _HANDLERS.get(canonical)
if handler is None and not name.startswith("/"):
slashed = f"/{name}"
canonical = _ALIASES.get(slashed, slashed)
handler = _HANDLERS.get(canonical)
if handler is None:
t = app.current_theme
app.console.print(f"[{t.error}]Unknown:[/{t.error}] {name} (try /help)")
app.console.print(f"[{t.error}]Unknown:[/{t.error}] {name} (try help)")
return
logger.info("event=repl_cmd_start cmd=%s", canonical)
try:
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/test_repl_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1455,3 +1455,38 @@ def test_clear_alias_c_routes_to_clear(
app.run()
captured = capsys.readouterr()
assert "\x1b[H\x1b[2J\x1b[3J" in captured.out


# ---------------------------------------------------------------------------
# Slashless commands — `sessions` works like `/sessions`
# ---------------------------------------------------------------------------


def test_repl_command_works_without_slash(tmp_path: Path) -> None:
"""Bare `quit` resolves through the same handler table as `/quit`."""
registry = _registry_with_project(tmp_path)
app = BachRepl(
registry,
input_fn=_scripted_input(["quit"]),
)
app.run() # would hang (or exhaust input) if `quit` weren't dispatched


def test_repl_bare_command_with_args(tmp_path: Path) -> None:
"""Args pass through the slashless path: `help board` == `/help board`."""
registry = _registry_with_project(tmp_path)
app = BachRepl(
registry,
input_fn=_scripted_input(["help board", "quit"]),
)
app.run() # /help handler runs with 'board'; loop survives to quit


def test_repl_bare_unknown_command_still_errors(tmp_path: Path) -> None:
"""A bare token that matches no command reports Unknown and survives."""
registry = _registry_with_project(tmp_path)
app = BachRepl(
registry,
input_fn=_scripted_input(["definitelynotacommand", "quit"]),
)
app.run()
Loading