From 2378e7cd30a096ce2e04177e530ab3cb13e5029c Mon Sep 17 00:00:00 2001 From: Jacob Taunton Date: Mon, 13 Apr 2026 08:22:41 -0700 Subject: [PATCH 1/2] fix: show short message IDs in table to prevent truncation UUIDs were being silently truncated by Rich table auto-sizing, making them unusable for `ax messages get`. Now displays first 8 chars (like git short hashes). Full IDs available via --json. Co-Authored-By: Cinder --- ax_cli/commands/messages.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ax_cli/commands/messages.py b/ax_cli/commands/messages.py index 1d52df2..ebbdea3 100644 --- a/ax_cli/commands/messages.py +++ b/ax_cli/commands/messages.py @@ -349,10 +349,12 @@ def list_messages( c = str(m.get("content", "")) m["content_short"] = c[:60] + "..." if len(c) > 60 else c m["sender"] = m.get("display_name") or m.get("sender_handle") or m.get("sender_type", "") + full_id = str(m.get("id", "")) + m["short_id"] = full_id[:8] if full_id else "" print_table( ["ID", "Sender", "Content", "Created At"], messages, - keys=["id", "sender", "content_short", "created_at"], + keys=["short_id", "sender", "content_short", "created_at"], ) From b163abd9fa4e55c3e26c0e1fdadd57acc99dd966 Mon Sep 17 00:00:00 2001 From: anvil Date: Mon, 13 Apr 2026 15:43:29 +0000 Subject: [PATCH 2/2] fix: resolve short message IDs --- ax_cli/commands/messages.py | 47 ++++++++++++++-- tests/test_handoff.py | 2 +- tests/test_messages.py | 109 ++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 7 deletions(-) diff --git a/ax_cli/commands/messages.py b/ax_cli/commands/messages.py index ebbdea3..8e38f6f 100644 --- a/ax_cli/commands/messages.py +++ b/ax_cli/commands/messages.py @@ -98,6 +98,39 @@ def _wait_for_reply(client, message_id: str, timeout: int = 60) -> dict | None: ) +def _message_items(data) -> list[dict]: + if isinstance(data, list): + return data + if isinstance(data, dict): + return data.get("messages", []) + return [] + + +def _resolve_message_id(client, message_id: str) -> str: + """Resolve table-friendly short message IDs against recent messages.""" + candidate = message_id.strip() + if not candidate or "-" in candidate or len(candidate) >= 32: + return candidate + + data = client.list_messages(limit=100) + matches = [ + str(message.get("id") or "") + for message in _message_items(data) + if str(message.get("id") or "").startswith(candidate) + ] + matches = [match for match in matches if match] + + if len(matches) == 1: + return matches[0] + if len(matches) > 1: + typer.echo( + f"Error: message ID prefix '{candidate}' is ambiguous. Use the full ID from --json.", + err=True, + ) + raise typer.Exit(1) + return candidate + + def _attachment_ref( *, attachment_id: str, @@ -294,11 +327,12 @@ def send( final_content = f"{mention} {content}" try: + parent_id = _resolve_message_id(client, parent) if parent else None data = client.send_message( sid, final_content, channel=channel, - parent_id=parent, + parent_id=parent_id, attachments=attachments or None, ) except httpx.HTTPStatusError as e: @@ -341,7 +375,7 @@ def list_messages( data = client.list_messages(limit=limit, channel=channel) except httpx.HTTPStatusError as e: handle_error(e) - messages = data if isinstance(data, list) else data.get("messages", []) + messages = _message_items(data) if as_json: print_json(messages) else: @@ -366,7 +400,7 @@ def get( """Get a single message.""" client = get_client() try: - data = client.get_message(message_id) + data = client.get_message(_resolve_message_id(client, message_id)) except httpx.HTTPStatusError as e: handle_error(e) if as_json: @@ -384,7 +418,7 @@ def edit( """Edit a message.""" client = get_client() try: - data = client.edit_message(message_id, content) + data = client.edit_message(_resolve_message_id(client, message_id), content) except httpx.HTTPStatusError as e: handle_error(e) if as_json: @@ -401,11 +435,12 @@ def delete( """Delete a message.""" client = get_client() try: - client.delete_message(message_id) + resolved_message_id = _resolve_message_id(client, message_id) + client.delete_message(resolved_message_id) except httpx.HTTPStatusError as e: handle_error(e) if as_json: - print_json({"status": "deleted", "message_id": message_id}) + print_json({"status": "deleted", "message_id": resolved_message_id}) else: typer.echo("Deleted.") diff --git a/tests/test_handoff.py b/tests/test_handoff.py index 45a5479..c40e05f 100644 --- a/tests/test_handoff.py +++ b/tests/test_handoff.py @@ -155,7 +155,7 @@ def test_handoff_is_registered_and_old_tone_verbs_are_removed(): handoff_help = runner.invoke(app, ["handoff", "--help"]) assert handoff_help.exit_code == 0 - assert "--follow-up" in handoff_help.output + assert "follow-up" in handoff_help.output old_command = runner.invoke(app, ["ship", "--help"]) assert old_command.exit_code != 0 diff --git a/tests/test_messages.py b/tests/test_messages.py index 7d5b364..8eff6ef 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -84,3 +84,112 @@ def send_message( assert attachment["content_type"] == "text/plain" assert attachment["size"] == sample.stat().st_size assert attachment["size_bytes"] == sample.stat().st_size + + +def test_messages_list_shows_short_ids_but_json_keeps_full_ids(monkeypatch): + message_id = "12345678-90ab-cdef-1234-567890abcdef" + + class FakeClient: + def list_messages(self, limit=20, channel="main"): + return { + "messages": [ + { + "id": message_id, + "content": "hello", + "display_name": "orion", + "created_at": "2026-04-13T15:00:00Z", + } + ] + } + + monkeypatch.setattr("ax_cli.commands.messages.get_client", lambda: FakeClient()) + + table_result = runner.invoke(app, ["messages", "list"]) + assert table_result.exit_code == 0, table_result.output + assert "12345678" in table_result.output + assert message_id not in table_result.output + + json_result = runner.invoke(app, ["messages", "list", "--json"]) + assert json_result.exit_code == 0, json_result.output + assert json.loads(json_result.output)[0]["id"] == message_id + + +def test_messages_get_resolves_short_id_prefix(monkeypatch): + message_id = "12345678-90ab-cdef-1234-567890abcdef" + calls = {} + + class FakeClient: + def list_messages(self, limit=20, channel="main"): + calls["list_limit"] = limit + return {"messages": [{"id": message_id}]} + + def get_message(self, requested_id): + calls["get_id"] = requested_id + return {"id": requested_id, "content": "hello"} + + monkeypatch.setattr("ax_cli.commands.messages.get_client", lambda: FakeClient()) + + result = runner.invoke(app, ["messages", "get", "12345678", "--json"]) + assert result.exit_code == 0, result.output + assert calls["list_limit"] == 100 + assert calls["get_id"] == message_id + assert json.loads(result.output)["id"] == message_id + + +def test_messages_send_resolves_short_parent_id(monkeypatch): + parent_id = "abcdef12-3456-7890-abcd-ef1234567890" + calls = {} + + class FakeClient: + _base_headers = {} + + def list_messages(self, limit=20, channel="main"): + calls["list_limit"] = limit + return {"messages": [{"id": parent_id}]} + + def send_message(self, space_id, content, *, channel="main", parent_id=None, attachments=None): + calls["message"] = { + "space_id": space_id, + "content": content, + "channel": channel, + "parent_id": parent_id, + "attachments": attachments, + } + return {"id": "reply-message-id"} + + monkeypatch.setattr("ax_cli.commands.messages.get_client", lambda: FakeClient()) + monkeypatch.setattr("ax_cli.commands.messages.resolve_space_id", lambda client, explicit=None: "space-1") + monkeypatch.setattr("ax_cli.commands.messages.resolve_agent_name", lambda client=None: None) + + result = runner.invoke(app, ["messages", "send", "reply", "--parent", "abcdef12", "--skip-ax", "--json"]) + assert result.exit_code == 0, result.output + assert calls["list_limit"] == 100 + assert calls["message"]["parent_id"] == parent_id + + +def test_messages_edit_and_delete_resolve_short_id_prefix(monkeypatch): + message_id = "12345678-90ab-cdef-1234-567890abcdef" + calls = {} + + class FakeClient: + def list_messages(self, limit=20, channel="main"): + calls["list_calls"] = calls.get("list_calls", 0) + 1 + return {"messages": [{"id": message_id}]} + + def edit_message(self, requested_id, content): + calls["edit"] = {"id": requested_id, "content": content} + return {"id": requested_id, "content": content} + + def delete_message(self, requested_id): + calls["delete_id"] = requested_id + + monkeypatch.setattr("ax_cli.commands.messages.get_client", lambda: FakeClient()) + + edit_result = runner.invoke(app, ["messages", "edit", "12345678", "updated", "--json"]) + assert edit_result.exit_code == 0, edit_result.output + assert calls["edit"] == {"id": message_id, "content": "updated"} + + delete_result = runner.invoke(app, ["messages", "delete", "12345678", "--json"]) + assert delete_result.exit_code == 0, delete_result.output + assert calls["delete_id"] == message_id + assert json.loads(delete_result.output)["message_id"] == message_id