diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 107d238977..0db89cecab 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -730,6 +730,41 @@ def test_complete_slash_surfaces_completer_error(monkeypatch): assert "no completer" in resp["error"]["message"] +def test_complete_slash_display_is_plain_string(monkeypatch): + """complete.slash must convert prompt_toolkit FormattedText display values + to plain strings before sending them over RPC. + + Regression: c.display is a FormattedText object, not a str. Passing it + raw caused JSON serialization failures and broken rendering in the TUI + completion dropdown. + """ + from prompt_toolkit.formatted_text import FormattedText + + class _FakeCompletion: + text = "/resume" + display = FormattedText([("class:command", "/resume")]) + display_meta = FormattedText([("", "Resume a session")]) + + with patch("hermes_cli.commands.SlashCommandCompleter") as MockCompleter: + instance = MockCompleter.return_value + instance.get_completions.return_value = [_FakeCompletion()] + + resp = server.handle_request( + {"id": "1", "method": "complete.slash", "params": {"text": "/res"}} + ) + + assert "result" in resp, resp + items = resp["result"]["items"] + assert len(items) >= 1 + item = next(i for i in items if i["text"] == "/resume") + assert isinstance(item["display"], str), ( + "display must be a plain str — FormattedText breaks JSON serialization" + ) + assert item["display"] == "/resume" + assert isinstance(item["meta"], str) + assert item["meta"] == "Resume a session" + + def test_input_detect_drop_attaches_image(monkeypatch): fake_cli = types.ModuleType("cli") fake_cli._detect_file_drop = lambda raw: { diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 7bc0fb2e09..9f6b92ce93 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -3650,7 +3650,7 @@ def _(rid, params: dict) -> dict: items = [ { "text": c.text, - "display": c.display or c.text, + "display": to_plain_text(c.display) if c.display else c.text, "meta": to_plain_text(c.display_meta) if c.display_meta else "", } for c in completer.get_completions(doc, None) diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index d856451751..9ed819b6d6 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -203,7 +203,7 @@ const ComposerPane = memo(function ComposerPane({