4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
187 lines
6.4 KiB
Python
187 lines
6.4 KiB
Python
"""Slash-command, alias, and file-path completion for the REPL prompt."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterable
|
|
|
|
from prompt_toolkit.application.current import get_app_or_none
|
|
from prompt_toolkit.completion import CompleteEvent, Completer, Completion, PathCompleter
|
|
from prompt_toolkit.document import Document
|
|
|
|
from platform.terminal import theme as ui_theme
|
|
from surfaces.interactive_shell.command_registry import SLASH_COMMANDS
|
|
from surfaces.interactive_shell.command_registry.help import QUICK_ACCESS_COMMANDS
|
|
from surfaces.interactive_shell.command_registry.types import SlashCommand
|
|
from surfaces.interactive_shell.ui.components.choice_menu import repl_tty_interactive
|
|
from surfaces.interactive_shell.ui.input_prompt.layout import (
|
|
_DEFAULT_TERMINAL_COLUMNS,
|
|
_clip_text,
|
|
_short_meta,
|
|
_terminal_columns,
|
|
)
|
|
|
|
_COMPLETION_PREVIEW_SEP = " — "
|
|
|
|
|
|
def _slash_command_name(completion: Completion) -> str | None:
|
|
for candidate in (completion.text, completion.display_text or ""):
|
|
if candidate.startswith("/"):
|
|
return candidate
|
|
return None
|
|
|
|
|
|
def _resolve_completion_preview(
|
|
completion: Completion,
|
|
*,
|
|
buffer_text: str,
|
|
) -> tuple[str, str] | None:
|
|
cmd_name = _slash_command_name(completion)
|
|
if cmd_name is not None:
|
|
entry = SLASH_COMMANDS.get(cmd_name)
|
|
if entry is not None:
|
|
return cmd_name, entry.description
|
|
|
|
meta = completion.display_meta_text
|
|
if not meta:
|
|
return None
|
|
|
|
display = completion.display_text or completion.text
|
|
if cmd_name is not None:
|
|
label = display
|
|
else:
|
|
parts = buffer_text.split()
|
|
label = f"{parts[0]} {display}" if parts and parts[0].startswith("/") else display
|
|
return label, meta
|
|
|
|
|
|
def completion_preview_hint_ansi() -> str:
|
|
"""Full description for the highlighted completion menu item."""
|
|
app = get_app_or_none()
|
|
if app is None:
|
|
return ""
|
|
buffer = app.current_buffer
|
|
complete_state = buffer.complete_state
|
|
if complete_state is None or not complete_state.completions:
|
|
return ""
|
|
|
|
completion = complete_state.current_completion or complete_state.completions[0]
|
|
preview = _resolve_completion_preview(completion, buffer_text=buffer.text)
|
|
if preview is None:
|
|
return ""
|
|
|
|
label, description = preview
|
|
try:
|
|
cols = app.output.get_size().columns
|
|
except Exception:
|
|
cols = _DEFAULT_TERMINAL_COLUMNS
|
|
line = _clip_text(f"{label}{_COMPLETION_PREVIEW_SEP}{description}", cols)
|
|
return f"{ui_theme.ANSI_DIM}{line}{ui_theme.ANSI_RESET}"
|
|
|
|
|
|
# Precomputed at import time so bare-`/` completions never rebuild it per keystroke.
|
|
_QUICK_ACCESS_SET: frozenset[str] = frozenset(QUICK_ACCESS_COMMANDS)
|
|
|
|
|
|
def _slash_completion(cmd: SlashCommand, start_position: int, *, cols: int) -> Completion:
|
|
return Completion(
|
|
cmd.name,
|
|
start_position=start_position,
|
|
display=cmd.name,
|
|
display_meta=_short_meta(cmd.description, command_name=cmd.name, cols=cols),
|
|
)
|
|
|
|
|
|
class ShellCompleter(Completer):
|
|
"""Tab-completion for slash commands, subcommands, and file paths."""
|
|
|
|
def get_completions(
|
|
self,
|
|
document: Document,
|
|
complete_event: CompleteEvent,
|
|
) -> Iterable[Completion]:
|
|
text = document.text_before_cursor
|
|
if not text:
|
|
return
|
|
|
|
if not text.startswith("/"):
|
|
return
|
|
|
|
parts = text.split()
|
|
trailing_space = text != text.rstrip(" ")
|
|
if len(parts) == 1 and not trailing_space:
|
|
needle = parts[0].lower()
|
|
cols = _terminal_columns()
|
|
if needle == "/":
|
|
# Bare `/`: show most important commands first, then the rest.
|
|
for name in QUICK_ACCESS_COMMANDS:
|
|
cmd = SLASH_COMMANDS.get(name)
|
|
if cmd is not None:
|
|
yield _slash_completion(cmd, -1, cols=cols)
|
|
for cmd in SLASH_COMMANDS.values():
|
|
if cmd.name not in _QUICK_ACCESS_SET:
|
|
yield _slash_completion(cmd, -1, cols=cols)
|
|
else:
|
|
for cmd in SLASH_COMMANDS.values():
|
|
if cmd.name.lower().startswith(needle):
|
|
yield _slash_completion(cmd, -len(parts[0]), cols=cols)
|
|
return
|
|
|
|
if len(parts) <= 2:
|
|
cmd_name = parts[0].lower()
|
|
raw_arg = "" if trailing_space or len(parts) < 2 else parts[1]
|
|
|
|
if _suppress_empty_arg_completions_for_inline_picker(cmd_name, raw_arg):
|
|
return
|
|
|
|
if cmd_name in ("/investigate", "/save"):
|
|
if cmd_name == "/investigate":
|
|
entry = SLASH_COMMANDS.get(cmd_name)
|
|
hints = entry.first_arg_completions if entry is not None else ()
|
|
sub_prefix = raw_arg.lower()
|
|
for sub, meta in hints:
|
|
if sub.startswith(sub_prefix):
|
|
yield Completion(
|
|
sub,
|
|
start_position=-len(raw_arg),
|
|
display=sub,
|
|
display_meta=meta,
|
|
)
|
|
yield from PathCompleter(expanduser=True).get_completions(
|
|
Document(raw_arg, len(raw_arg)),
|
|
complete_event,
|
|
)
|
|
return
|
|
|
|
entry = SLASH_COMMANDS.get(cmd_name)
|
|
hints = entry.first_arg_completions if entry is not None else ()
|
|
sub_prefix = raw_arg.lower()
|
|
for sub, meta in hints:
|
|
if sub.startswith(sub_prefix):
|
|
yield Completion(
|
|
sub,
|
|
start_position=-len(raw_arg),
|
|
display=sub,
|
|
display_meta=meta,
|
|
)
|
|
|
|
|
|
# Commands where bare invocation opens an inline picker in TTY mode.
|
|
_INLINE_PICKER_COMMANDS: frozenset[str] = frozenset(
|
|
{
|
|
"/history",
|
|
"/integrations",
|
|
"/investigate",
|
|
"/mcp",
|
|
"/model",
|
|
"/template",
|
|
"/tests",
|
|
"/trust",
|
|
"/verbose",
|
|
}
|
|
)
|
|
|
|
|
|
def _suppress_empty_arg_completions_for_inline_picker(cmd_name: str, raw_arg: str) -> bool:
|
|
"""Hide first-arg autocomplete when bare slash command opens inline picker."""
|
|
return repl_tty_interactive() and not raw_arg and cmd_name in _INLINE_PICKER_COMMANDS
|