feat(ai): /ai start|stop agent control + in-room typing indicator

Owner of the spawning client can summon/dismiss a local AI agent from inside
the room (default ollama/qwen2.5:3b); the agent emits encrypted typing frames
that drive a "thinking" spinner in the client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-01 11:38:15 -07:00
parent 54b7637ec8
commit 05bdc2d802
4 changed files with 204 additions and 4 deletions
+9
View File
@@ -56,8 +56,15 @@ class AgentBridge(Client):
return None
return rest # sole-agent form: `/ai <question>`
async def _send_typing(self, ws, on: bool) -> None:
"""Tell the room our reply is (not) being generated, so clients can show
a spinner. A control frame — never displayed as chat."""
frame = json.dumps({"_ai": "typing", "name": self.name, "on": on})
await ws.send(self.room_fernet.encrypt(frame.encode()).decode())
async def _answer(self, ws, question: str, asker: str) -> None:
self.transcript.append(Msg("user", f"{asker}: {question}"))
await self._send_typing(ws, True)
try:
reply = await asyncio.to_thread(
self.provider.complete,
@@ -66,6 +73,8 @@ class AgentBridge(Client):
)
except Exception as e: # noqa: BLE001 — surface any provider failure in-room
reply = f"[ai error: {e}]"
finally:
await self._send_typing(ws, False)
reply = reply.strip() or "[empty reply]"
self.transcript.append(Msg("assistant", reply))
await ws.send(self.room_fernet.encrypt(reply.encode()).decode())