use crate::websocket::ServerMessage;
use std::io::IsTerminal;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
Interactive,
NonInteractive,
Jsonl,
WebSocket,
}
impl OutputMode {
pub fn from_cli_arg(mode: &str, is_terminal: bool) -> Self {
match mode {
"jsonl" => Self::Jsonl,
"plain" if is_terminal => Self::Interactive,
"plain" => Self::NonInteractive,
_ => {
if is_terminal {
Self::Interactive
} else {
Self::NonInteractive
}
}
}
}
pub fn from_runtime_mode(mode: &str) -> Self {
match mode {
"interactive" => Self::Interactive,
"jsonl" => Self::Jsonl,
"websocket" => Self::WebSocket,
_ => Self::NonInteractive,
}
}
pub fn is_interactive(self) -> bool {
matches!(self, Self::Interactive)
}
pub fn should_show_animations(self) -> bool {
matches!(self, Self::Interactive)
}
pub fn should_suppress_cli_output(self) -> bool {
matches!(self, Self::Jsonl | Self::WebSocket)
}
pub fn is_terminal_mode(self) -> bool {
matches!(self, Self::Interactive | Self::NonInteractive)
}
}
pub trait OutputSink: Clone {
fn emit(&self, msg: ServerMessage);
}
#[derive(Clone, Copy)]
pub struct SilentSink;
impl OutputSink for SilentSink {
#[inline]
fn emit(&self, _msg: ServerMessage) {
}
}
#[derive(Clone, Copy)]
pub struct JsonlSink;
impl OutputSink for JsonlSink {
#[inline]
fn emit(&self, msg: ServerMessage) {
if let Ok(json) = serde_json::to_string(&msg) {
println!("{}", json);
}
}
}
#[derive(Clone)]
pub struct WebSocketSink {
tx: tokio::sync::mpsc::UnboundedSender<ServerMessage>,
}
impl WebSocketSink {
pub fn new(tx: tokio::sync::mpsc::UnboundedSender<ServerMessage>) -> Self {
Self { tx }
}
}
impl OutputSink for WebSocketSink {
#[inline]
fn emit(&self, msg: ServerMessage) {
let _ = self.tx.send(msg);
}
}
pub fn detect_output_mode(cli_mode: &str) -> OutputMode {
OutputMode::from_cli_arg(cli_mode, std::io::stdin().is_terminal())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::websocket::AssistantPayload;
#[test]
fn test_output_mode_from_cli_arg() {
assert_eq!(OutputMode::from_cli_arg("jsonl", true), OutputMode::Jsonl);
assert_eq!(OutputMode::from_cli_arg("jsonl", false), OutputMode::Jsonl);
assert_eq!(
OutputMode::from_cli_arg("plain", true),
OutputMode::Interactive
);
assert_eq!(
OutputMode::from_cli_arg("plain", false),
OutputMode::NonInteractive
);
assert_eq!(
OutputMode::from_cli_arg("unknown", true),
OutputMode::Interactive
);
assert_eq!(
OutputMode::from_cli_arg("unknown", false),
OutputMode::NonInteractive
);
}
#[test]
fn test_output_mode_from_runtime_mode() {
assert_eq!(
OutputMode::from_runtime_mode("interactive"),
OutputMode::Interactive
);
assert_eq!(
OutputMode::from_runtime_mode("plain"),
OutputMode::NonInteractive
);
assert_eq!(OutputMode::from_runtime_mode("jsonl"), OutputMode::Jsonl);
assert_eq!(
OutputMode::from_runtime_mode("websocket"),
OutputMode::WebSocket
);
assert_eq!(
OutputMode::from_runtime_mode("unknown"),
OutputMode::NonInteractive
);
}
#[test]
fn test_output_mode_is_interactive() {
assert!(OutputMode::Interactive.is_interactive());
assert!(!OutputMode::NonInteractive.is_interactive());
assert!(!OutputMode::Jsonl.is_interactive());
assert!(!OutputMode::WebSocket.is_interactive());
}
#[test]
fn test_output_mode_should_show_animations() {
assert!(OutputMode::Interactive.should_show_animations());
assert!(!OutputMode::NonInteractive.should_show_animations());
assert!(!OutputMode::Jsonl.should_show_animations());
assert!(!OutputMode::WebSocket.should_show_animations());
}
#[test]
fn test_output_mode_should_suppress_cli_output() {
assert!(!OutputMode::Interactive.should_suppress_cli_output());
assert!(!OutputMode::NonInteractive.should_suppress_cli_output());
assert!(OutputMode::Jsonl.should_suppress_cli_output());
assert!(OutputMode::WebSocket.should_suppress_cli_output());
}
#[test]
fn test_output_mode_is_terminal_mode() {
assert!(OutputMode::Interactive.is_terminal_mode());
assert!(OutputMode::NonInteractive.is_terminal_mode());
assert!(!OutputMode::Jsonl.is_terminal_mode());
assert!(!OutputMode::WebSocket.is_terminal_mode());
}
#[test]
fn test_silent_sink_discards_messages() {
let sink = SilentSink;
let msg = ServerMessage::Assistant(AssistantPayload {
content: "test".to_string(),
session_id: "session_123".to_string(),
});
sink.emit(msg);
}
#[test]
fn test_jsonl_sink_emits_valid_json() {
let sink = JsonlSink;
let msg = ServerMessage::Assistant(AssistantPayload {
content: "test content".to_string(),
session_id: "session_123".to_string(),
});
sink.emit(msg);
}
#[test]
fn test_websocket_sink_sends_through_channel() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let sink = WebSocketSink::new(tx);
let msg = ServerMessage::Assistant(AssistantPayload {
content: "test".to_string(),
session_id: "session_123".to_string(),
});
sink.emit(msg);
let received = rx.try_recv().unwrap();
assert!(
matches!(received, ServerMessage::Assistant(AssistantPayload { content, .. }) if content == "test")
);
}
#[test]
fn test_websocket_sink_handles_closed_channel() {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let sink = WebSocketSink::new(tx);
drop(rx);
let msg = ServerMessage::Assistant(AssistantPayload {
content: "test".to_string(),
session_id: "session_123".to_string(),
});
sink.emit(msg);
}
}