1use crate::io::{IOOutput, IOPort, InputCommand, InputParser};
62use orcs_event::{Signal, SignalKind};
63use orcs_types::{ChannelId, Principal, SignalScope};
64
65pub struct IOBridge {
76 io_port: IOPort,
78 channel_id: ChannelId,
80 parser: InputParser,
82}
83
84impl IOBridge {
85 #[must_use]
91 pub fn new(io_port: IOPort) -> Self {
92 Self::with_parser(io_port, InputParser)
93 }
94
95 #[must_use]
99 pub fn with_parser(io_port: IOPort, parser: InputParser) -> Self {
100 let channel_id = io_port.channel_id();
101 Self {
102 io_port,
103 channel_id,
104 parser,
105 }
106 }
107
108 #[must_use]
110 pub fn channel_id(&self) -> ChannelId {
111 self.channel_id
112 }
113
114 #[must_use]
122 pub fn command_to_signal(
123 &self,
124 cmd: &InputCommand,
125 principal: &Principal,
126 default_approval_id: Option<&str>,
127 ) -> Option<Signal> {
128 cmd.to_signal(principal.clone(), default_approval_id)
129 }
130
131 #[must_use]
139 pub fn parse_line_to_signal(
140 &self,
141 line: &str,
142 principal: &Principal,
143 default_approval_id: Option<&str>,
144 ) -> Option<Signal> {
145 let cmd = self.parser.parse(line);
146 self.command_to_signal(&cmd, principal, default_approval_id)
147 }
148
149 pub fn drain_input_to_signals(
161 &mut self,
162 principal: &Principal,
163 ) -> (Vec<Signal>, Vec<InputCommand>) {
164 let mut signals = Vec::new();
165 let mut other_commands = Vec::new();
166
167 for input in self.io_port.drain_input() {
168 match input {
169 crate::io::IOInput::Line { text, context } => {
170 let cmd = self.parser.parse(&text);
171 let approval_id = context.approval_id.as_deref();
172 if let Some(signal) = self.command_to_signal(&cmd, principal, approval_id) {
173 signals.push(signal);
174 } else {
175 other_commands.push(cmd);
176 }
177 }
178 crate::io::IOInput::Signal(kind) => {
179 let scope = match &kind {
180 SignalKind::Veto => SignalScope::Global,
181 _ => SignalScope::Channel(self.channel_id),
182 };
183 signals.push(Signal::new(kind, scope, principal.clone()));
184 }
185 crate::io::IOInput::Eof => {
186 other_commands.push(InputCommand::Quit);
187 }
188 }
189 }
190
191 (signals, other_commands)
192 }
193
194 pub async fn recv_input(
210 &mut self,
211 principal: &Principal,
212 ) -> Option<Result<Signal, InputCommand>> {
213 let input = self.io_port.recv().await?;
214
215 match input {
216 crate::io::IOInput::Line { text, context } => {
217 let cmd = self.parser.parse(&text);
218 let approval_id = context.approval_id.as_deref();
219 if let Some(signal) = self.command_to_signal(&cmd, principal, approval_id) {
220 Some(Ok(signal))
221 } else {
222 Some(Err(cmd))
223 }
224 }
225 crate::io::IOInput::Signal(kind) => {
226 let scope = match &kind {
227 SignalKind::Veto => SignalScope::Global,
228 _ => SignalScope::Channel(self.channel_id),
229 };
230 Some(Ok(Signal::new(kind, scope, principal.clone())))
231 }
232 crate::io::IOInput::Eof => Some(Err(InputCommand::Quit)),
233 }
234 }
235
236 pub async fn send_output(
244 &self,
245 output: IOOutput,
246 ) -> Result<(), tokio::sync::mpsc::error::SendError<IOOutput>> {
247 self.io_port.send(output).await
248 }
249
250 pub async fn show_processing(
252 &self,
253 component: &str,
254 operation: &str,
255 ) -> Result<(), tokio::sync::mpsc::error::SendError<IOOutput>> {
256 self.io_port
257 .send(IOOutput::processing(component, operation))
258 .await
259 }
260
261 pub async fn show_approval_request(
263 &self,
264 request: &super::ApprovalRequest,
265 ) -> Result<(), tokio::sync::mpsc::error::SendError<IOOutput>> {
266 self.io_port.send(IOOutput::approval_request(request)).await
267 }
268
269 pub async fn show_approved(
271 &self,
272 approval_id: &str,
273 ) -> Result<(), tokio::sync::mpsc::error::SendError<IOOutput>> {
274 self.io_port.send(IOOutput::approved(approval_id)).await
275 }
276
277 pub async fn show_rejected(
279 &self,
280 approval_id: &str,
281 reason: Option<&str>,
282 ) -> Result<(), tokio::sync::mpsc::error::SendError<IOOutput>> {
283 self.io_port
284 .send(IOOutput::rejected(approval_id, reason.map(String::from)))
285 .await
286 }
287
288 pub async fn info(
290 &self,
291 message: &str,
292 ) -> Result<(), tokio::sync::mpsc::error::SendError<IOOutput>> {
293 self.io_port.send(IOOutput::info(message)).await
294 }
295
296 pub async fn warn(
298 &self,
299 message: &str,
300 ) -> Result<(), tokio::sync::mpsc::error::SendError<IOOutput>> {
301 self.io_port.send(IOOutput::warn(message)).await
302 }
303
304 pub async fn error(
306 &self,
307 message: &str,
308 ) -> Result<(), tokio::sync::mpsc::error::SendError<IOOutput>> {
309 self.io_port.send(IOOutput::error(message)).await
310 }
311
312 pub async fn prompt(
314 &self,
315 message: &str,
316 ) -> Result<(), tokio::sync::mpsc::error::SendError<IOOutput>> {
317 self.io_port.send(IOOutput::prompt(message)).await
318 }
319
320 #[must_use]
322 pub fn is_output_closed(&self) -> bool {
323 self.io_port.is_output_closed()
324 }
325}
326
327impl std::fmt::Debug for IOBridge {
328 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329 f.debug_struct("IOBridge")
330 .field("channel_id", &self.channel_id)
331 .finish_non_exhaustive()
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use crate::io::IOInput;
339 use orcs_types::PrincipalId;
340
341 fn test_principal() -> Principal {
342 Principal::User(PrincipalId::new())
343 }
344
345 fn setup() -> (
346 IOBridge,
347 crate::io::IOInputHandle,
348 crate::io::IOOutputHandle,
349 ) {
350 let channel_id = ChannelId::new();
351 let (port, input_handle, output_handle) = IOPort::with_defaults(channel_id);
352 let bridge = IOBridge::new(port);
353 (bridge, input_handle, output_handle)
354 }
355
356 #[test]
357 fn io_bridge_creation() {
358 let channel_id = ChannelId::new();
359 let (port, _, _) = IOPort::with_defaults(channel_id);
360 let bridge = IOBridge::new(port);
361 assert_eq!(bridge.channel_id(), channel_id);
362 }
363
364 #[test]
365 fn parse_line_to_signal_approve() {
366 let (bridge, _, _) = setup();
367 let principal = test_principal();
368
369 let signal = bridge.parse_line_to_signal("y", &principal, None);
371 assert!(signal.is_none());
372
373 let signal = bridge.parse_line_to_signal("y req-123", &principal, None);
375 assert!(signal.is_some());
376 assert!(signal.expect("should produce approve signal").is_approve());
377 }
378
379 #[test]
380 fn parse_line_to_signal_with_default() {
381 let (bridge, _, _) = setup();
382 let principal = test_principal();
383
384 let signal = bridge.parse_line_to_signal("y", &principal, Some("default-id"));
386 assert!(signal.is_some());
387
388 let signal = signal.expect("should produce signal with default approval id");
389 assert!(signal.is_approve());
390 if let SignalKind::Approve { approval_id } = &signal.kind {
391 assert_eq!(approval_id, "default-id");
392 }
393 }
394
395 #[test]
396 fn parse_line_to_signal_veto() {
397 let (bridge, _, _) = setup();
398 let principal = test_principal();
399
400 let signal = bridge.parse_line_to_signal("veto", &principal, None);
401 assert!(signal.is_some());
402 assert!(signal.expect("should produce veto signal").is_veto());
403 }
404
405 #[test]
406 fn parse_line_to_signal_quit_returns_none() {
407 let (bridge, _, _) = setup();
408 let principal = test_principal();
409
410 let signal = bridge.parse_line_to_signal("q", &principal, None);
412 assert!(signal.is_none());
413 }
414
415 #[tokio::test]
416 async fn drain_input_to_signals() {
417 use crate::io::InputContext;
418
419 let (mut bridge, input_handle, _output_handle) = setup();
420 let principal = test_principal();
421 let ctx = InputContext::with_approval_id("pending-1");
422
423 input_handle
425 .send(IOInput::line_with_context("y", ctx.clone()))
426 .await
427 .expect("send approve input should succeed");
428 input_handle
429 .send(IOInput::line_with_context("n", ctx.clone()))
430 .await
431 .expect("send reject input should succeed");
432 input_handle
433 .send(IOInput::line_with_context("veto", ctx.clone()))
434 .await
435 .expect("send veto input should succeed");
436 input_handle
437 .send(IOInput::line("q"))
438 .await
439 .expect("send quit input should succeed");
440 input_handle
441 .send(IOInput::line("unknown"))
442 .await
443 .expect("send unknown input should succeed");
444
445 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
447
448 let (signals, commands) = bridge.drain_input_to_signals(&principal);
449
450 assert_eq!(signals.len(), 3);
452 assert!(signals[0].is_approve());
453 assert!(signals[1].is_reject());
454 assert!(signals[2].is_veto());
455
456 assert_eq!(commands.len(), 2);
458 assert!(matches!(commands[0], InputCommand::Quit));
459 assert!(matches!(commands[1], InputCommand::Unknown { .. }));
460 }
461
462 #[tokio::test]
463 async fn recv_input_signal() {
464 use crate::io::InputContext;
465
466 let (mut bridge, input_handle, _output_handle) = setup();
467 let principal = test_principal();
468 let ctx = InputContext::with_approval_id("req-1");
469
470 input_handle
471 .send(IOInput::line_with_context("y", ctx))
472 .await
473 .expect("send approve input should succeed");
474
475 let result = bridge.recv_input(&principal).await;
476 assert!(result.is_some());
477 assert!(result.expect("recv_input should return Some").is_ok());
478 }
479
480 #[tokio::test]
481 async fn recv_input_non_signal() {
482 let (mut bridge, input_handle, _output_handle) = setup();
483 let principal = test_principal();
484
485 input_handle
486 .send(IOInput::line("q"))
487 .await
488 .expect("send quit input should succeed");
489
490 let result = bridge.recv_input(&principal).await;
491 assert!(result.is_some());
492 let cmd = result
493 .expect("recv_input should return Some")
494 .expect_err("quit should not map to a signal");
495 assert!(matches!(cmd, InputCommand::Quit));
496 }
497
498 #[tokio::test]
499 async fn send_output() {
500 let (bridge, _input_handle, mut output_handle) = setup();
501
502 bridge
503 .info("test message")
504 .await
505 .expect("send info should succeed");
506
507 let output = output_handle.recv().await.expect("should receive output");
508 assert!(matches!(output, IOOutput::Print { .. }));
509 }
510
511 #[tokio::test]
512 async fn show_processing() {
513 let (bridge, _input_handle, mut output_handle) = setup();
514
515 bridge
516 .show_processing("agent_mgr", "input")
517 .await
518 .expect("send should succeed");
519
520 let output = output_handle.recv().await.expect("should receive output");
521 match output {
522 IOOutput::ShowProcessing {
523 component,
524 operation,
525 } => {
526 assert_eq!(component, "agent_mgr");
527 assert_eq!(operation, "input");
528 }
529 other => panic!("Expected ShowProcessing, got {:?}", other),
530 }
531 }
532
533 #[tokio::test]
534 async fn show_approval_request() {
535 let (bridge, _input_handle, mut output_handle) = setup();
536
537 let req = super::super::ApprovalRequest::with_id(
538 "req-123",
539 "write",
540 "Write file",
541 serde_json::json!({}),
542 );
543 bridge
544 .show_approval_request(&req)
545 .await
546 .expect("send approval request should succeed");
547
548 let output = output_handle
549 .recv()
550 .await
551 .expect("should receive approval request output");
552 if let IOOutput::ShowApprovalRequest { id, operation, .. } = output {
553 assert_eq!(id, "req-123");
554 assert_eq!(operation, "write");
555 } else {
556 panic!("Expected ShowApprovalRequest");
557 }
558 }
559
560 #[test]
561 fn debug_impl() {
562 let (bridge, _, _) = setup();
563 let debug_str = format!("{:?}", bridge);
564 assert!(debug_str.contains("IOBridge"));
565 assert!(debug_str.contains("channel_id"));
566 }
567}