feat(relay): rich Slack status-line parity — advertise supports_status_text, carry live per-tool phrase on typing frames (QA-1)

Native Slack shows dynamic assistant-status text ('Finding answers…',
'is running pytest…') because SlackAdapter sets supports_status_text=True
and renders the set_status_text() phrase in send_typing. The relay lane
advertised nothing, so run.py's live-status lane never fed it phrases and
the connector fell back to the static default.

- supports_status_text: descriptor-gated property (Slack only; other
  fronted platforms keep textless bubbles)
- send_typing: carry the stashed phrase as the typing op's content; omit
  when unset (empty string is Slack's explicit clear, reserved for
  stop_typing). Connector already renders content via
  assistant.threads.setStatus (#154).
This commit is contained in:
Victor Kyriazakos 2026-07-27 14:04:46 +00:00
parent be9de31967
commit b493bf63c7
2 changed files with 73 additions and 5 deletions

View file

@ -133,6 +133,23 @@ class RelayAdapter(BasePlatformAdapter):
def message_len_fn(self) -> Callable[[str], int]:
return _LEN_FNS.get(self.descriptor.len_unit, len)
@property
def supports_status_text(self) -> bool: # type: ignore[override]
"""Whether the fronted platform renders a TEXT status line.
Native parity (QA-1 rich status): Slack's typing surface is the
assistant status line ("Finding answers…" next to the bot name), a
text-rendering indicator. When the relay fronts Slack, advertise it so
run.py's live-status lane feeds per-tool phrases via
``set_status_text()`` exactly the wiring the native SlackAdapter
gets (``supports_status_text = True``). Other fronted platforms keep
textless typing bubbles and must NOT receive phrase traffic.
Property (not class attr) because ONE RelayAdapter class fronts many
platforms; the answer depends on the handshaked descriptor.
"""
return self.descriptor.platform == Platform.SLACK.value
def supports_draft_streaming(
self,
chat_type: Optional[str] = None,
@ -892,13 +909,26 @@ class RelayAdapter(BasePlatformAdapter):
"""
if self._transport is None:
return
# Rich status parity (QA-1): run.py's live-status lane stashes the
# current per-tool phrase via set_status_text() (base class store).
# Carry it as the typing frame's content so the connector's Slack
# sender renders it on assistant.threads.setStatus — the same phrase
# the native adapter shows ("is running pytest…", "Finding answers…").
# Absent (None/empty) => omit content; the connector falls back to its
# default "is typing…" heartbeat, preserving pre-phrase behaviour on
# every platform. Never send empty-string content here: on Slack that
# is the explicit CLEAR request reserved for stop_typing.
frame: Dict[str, Any] = {
"op": "typing",
"chat_id": chat_id,
"metadata": self._with_scope(chat_id, metadata),
}
phrase = getattr(self, "_status_text", {}).get(str(chat_id))
if phrase:
frame["content"] = str(phrase)
try:
await self._transport.send_outbound(
{
"op": "typing",
"chat_id": chat_id,
"metadata": self._with_scope(chat_id, metadata),
},
frame,
platform=self._platform_by_chat.get(str(chat_id)),
)
except Exception: # noqa: BLE001 - typing is cosmetic, never breaks a turn

View file

@ -208,3 +208,41 @@ async def test_non_slack_dm_approval_keeps_thread_id():
await adapter.send_exec_approval("dc1", "cmd", "s", metadata=md)
frame = _last_prompt(stub)
assert frame["metadata"]["thread_id"] == "9000"
# ---------------------------------------------------------------------------
# QA-1 rich status: the relay advertises Slack's text status line and carries
# the live per-tool phrase on the typing frame (native set_status_text parity).
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_slack_relay_advertises_status_text():
adapter, _stub = _wire("D1", "dm")
assert adapter.supports_status_text is True
@pytest.mark.asyncio
async def test_non_slack_relay_does_not_advertise_status_text():
stub = StubConnector(_slack_desc(platform="discord"))
adapter = RelayAdapter(
PlatformConfig(), _slack_desc(platform="discord"), transport=stub
)
assert adapter.supports_status_text is False
@pytest.mark.asyncio
async def test_typing_carries_live_status_phrase():
"""set_status_text() -> the next typing frame carries the phrase as
content; clearing it (None) reverts to a content-less heartbeat frame
(never an empty string, which is Slack's explicit clear)."""
adapter, stub = _wire("D1", "dm", scope_id="T1")
adapter.set_status_text("D1", "is running pytest…")
await adapter.send_typing("D1", metadata={"scope_id": "T1"})
typing = [f for f in stub.sent if f["op"] == "typing"]
assert typing and typing[-1].get("content") == "is running pytest…"
adapter.set_status_text("D1", None)
await adapter.send_typing("D1", metadata={"scope_id": "T1"})
typing = [f for f in stub.sent if f["op"] == "typing"]
assert "content" not in typing[-1], (
"cleared phrase must omit content (empty string means CLEAR on Slack)"
)