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
+20
View File
@@ -178,6 +178,13 @@ fn decode_msg(room: &fernet::Fernet, m: &Value, live: bool) -> Decoded {
Decoded::Skip
};
}
if t.starts_with("{\"_ai\":") {
return if live {
parse_ai(&t).map(Decoded::Sbx).unwrap_or(Decoded::Skip)
} else {
Decoded::Skip
};
}
(t, false)
}
Err(_) => ("[unreadable — wrong room password?]".to_string(), true),
@@ -220,6 +227,19 @@ fn parse_sbx(text: &str, sender: &str) -> Option<Net> {
}
}
/// Parse a decrypted `{"_ai":"typing",...}` frame — an AI agent signalling that
/// it is (or has finished) generating a reply, so the UI can show a spinner.
fn parse_ai(text: &str) -> Option<Net> {
let v: Value = serde_json::from_str(text).ok()?;
if v["_ai"].as_str()? != "typing" {
return None;
}
Some(Net::AiTyping {
name: v["name"].as_str().unwrap_or("ai").to_string(),
on: v["on"].as_bool().unwrap_or(false),
})
}
/// Parse a decrypted `{"_perm":"acl",...}` frame.
fn parse_perm(text: &str) -> Option<Net> {
let v: Value = serde_json::from_str(text).ok()?;