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
+26 -1
View File
@@ -128,6 +128,15 @@ fn draw_help(f: &mut Frame, area: Rect, theme: &Theme) {
),
kv("/sbx stop", "tear down the sandbox (purges the VM)"),
kv("/drive", "type into the shared shell (Esc releases)"),
kv(
"/ai start [model]",
"spawn a local AI agent (default ollama/qwen2.5:3b)",
),
kv("/ai stop", "dismiss the agent you started"),
kv(
"/ai <question>",
"ask an AI agent in the room (/ai <name> <q> if many)",
),
kv(
"/grant <user>",
"let a member drive the shell (owner)",
@@ -357,6 +366,17 @@ fn draw_roster(f: &mut Frame, area: ratatui::layout::Rect, app: &App, theme: &Th
f.render_widget(roster, area);
}
/// Animated "⠋ oracle is thinking…" title shown while AI agents generate a reply.
fn ai_thinking_title(app: &App) -> String {
const FRAMES: [&str; 10] = ["", "", "", "", "", "", "", "", "", ""];
let glyph = FRAMES[(app.spin / 2) % FRAMES.len()];
let mut names: Vec<&str> = app.ai_typing.iter().map(String::as_str).collect();
names.sort_unstable();
let who = names.join(", ");
let verb = if names.len() > 1 { "are" } else { "is" };
format!(" {glyph} {who} {verb} thinking… ")
}
fn draw_input(f: &mut Frame, area: ratatui::layout::Rect, app: &App, theme: &Theme) {
let input = Paragraph::new(Line::from(vec![
Span::styled("> ", Style::default().fg(theme.accent)),
@@ -378,9 +398,14 @@ fn draw_input(f: &mut Frame, area: ratatui::layout::Rect, app: &App, theme: &The
None if app.driving => {
format!(" {} DRIVING the shell — Esc to release ", theme.sigil)
}
None if !app.ai_typing.is_empty() => ai_thinking_title(app),
None => " message · enter send · /drive for shell · ctrl-q quit ".to_string(),
},
Style::default().fg(theme.title),
Style::default().fg(if app.ai_typing.is_empty() {
theme.title
} else {
theme.accent
}),
)),
);
f.render_widget(input, area);