1use crate::pxb;
29use std::collections::HashMap;
30use std::future::Future;
31use std::io;
32use std::pin::Pin;
33
34pub use crate::pxb::Error;
35
36mod schema;
37pub use schema::Schema;
38
39type Rd = io::StdinLock<'static>;
40type Wr = io::StdoutLock<'static>;
41type EventHandlers = HashMap<u16, Box<dyn FnMut(pxb::EventNotify)>>;
42
43#[derive(Debug, Clone, Default)]
46pub struct HostInfo {
47 pub cwd: String,
48 pub session_id: String,
49 pub extension_dir: String,
50 pub phi_version: String,
51}
52
53#[allow(clippy::type_complexity)] pub struct Tool {
61 pub name: String,
62 pub description: String,
63 pub schema: Schema,
64 pub readable: bool,
67 pub timeout_sec: u32,
69 pub detail_from_args: Option<Box<dyn FnMut(&[u8]) -> String>>,
71 pub execute: Box<dyn FnMut(&[u8]) -> Pin<Box<dyn Future<Output = Result<ToolResult, String>>>>>,
75}
76
77impl Tool {
78 pub fn new(
79 name: impl Into<String>,
80 description: impl Into<String>,
81 schema: impl Into<Schema>,
82 mut execute: impl FnMut(&[u8]) -> Result<ToolResult, String> + 'static,
83 ) -> Self {
84 Self {
85 name: name.into(),
86 description: description.into(),
87 schema: schema.into(),
88 timeout_sec: 0,
89 detail_from_args: None,
90 readable: false,
91 execute: Box::new(move |args| {
93 let result = execute(args);
94 Box::pin(async move { result })
95 }),
96 }
97 }
98
99 pub fn new_async<F, Fut>(
105 name: impl Into<String>,
106 description: impl Into<String>,
107 schema: impl Into<Schema>,
108 mut execute: F,
109 ) -> Self
110 where
111 F: FnMut(Vec<u8>) -> Fut + 'static,
112 Fut: Future<Output = Result<ToolResult, String>> + 'static,
113 {
114 Self {
115 name: name.into(),
116 description: description.into(),
117 schema: schema.into(),
118 timeout_sec: 0,
119 readable: false,
120 detail_from_args: None,
121 execute: Box::new(move |args| Box::pin(execute(args.to_vec()))),
122 }
123 }
124
125 pub fn timeout_sec(mut self, secs: u32) -> Self {
127 self.timeout_sec = secs;
128 self
129 }
130
131 pub fn detail_from_args(mut self, f: impl FnMut(&[u8]) -> String + 'static) -> Self {
133 self.detail_from_args = Some(Box::new(f));
134 self
135 }
136
137 pub fn readable(mut self) -> Self {
140 self.readable = true;
141 self
142 }
143}
144
145#[derive(Debug, Clone, Default)]
147pub struct ToolResult {
148 pub content: String,
149 pub detail: String,
150 pub output: String,
151}
152
153#[allow(clippy::type_complexity)] pub struct Command {
157 pub description: String,
158 pub needs_args: bool,
160 pub handler: Box<dyn FnMut(&str, &mut Context<'_>) -> Result<(), String>>,
161}
162
163impl Command {
164 pub fn new(
165 description: impl Into<String>,
166 handler: impl FnMut(&str, &mut Context<'_>) -> Result<(), String> + 'static,
167 ) -> Self {
168 Self {
169 description: description.into(),
170 needs_args: false,
171 handler: Box::new(handler),
172 }
173 }
174
175 pub fn needs_args(mut self) -> Self {
178 self.needs_args = true;
179 self
180 }
181}
182
183#[derive(Debug, Clone, Default, serde::Serialize)]
185#[serde(rename_all = "PascalCase")] pub struct ConfirmRequest {
187 pub title: String,
188 pub message: String,
189 pub yes: String, pub no: String, pub danger: bool,
192}
193
194#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
196pub struct ConfirmReply {
197 pub ok: bool,
198}
199
200#[derive(Debug, Clone, Default)]
201pub struct ToolCallEvent {
202 pub tool_name: String,
203 pub tool_call_id: String,
204 pub input: Vec<u8>,
205}
206
207#[derive(Debug, Clone, Default)]
208pub struct ToolCallResult {
209 pub block: bool,
210 pub reason: String,
211 pub input: Option<Vec<u8>>,
213 pub context: String,
214}
215
216#[derive(Debug, Clone, Default)]
217pub struct ToolResultEvent {
218 pub tool_name: String,
219 pub tool_call_id: String,
220 pub input: Vec<u8>,
221 pub content: String,
222 pub is_error: bool,
223 pub err: String,
224}
225
226#[derive(Debug, Clone, Default)]
227pub struct ToolResultResult {
228 pub content: Option<String>,
230 pub context: String,
231 pub stop: bool,
233 pub reason: String,
234}
235
236#[derive(Debug, Clone, Default)]
237pub struct BeforeAgentStartEvent {
238 pub prompt: String,
239}
240
241#[derive(Debug, Clone, Default)]
242pub struct BeforeAgentStartResult {
243 pub prompt: Option<String>,
245 pub system_prompt_append: String,
246}
247
248#[derive(Debug, Clone, Default)]
249pub struct SessionBeforeSwitchEvent {
250 pub reason: String,
251 pub target_session_id: String,
252}
253
254#[derive(Debug, Clone, Default)]
255pub struct SessionBeforeSwitchResult {
256 pub cancel: bool,
257 pub reason: String,
258 pub toast: String,
259}
260
261#[derive(Debug, Clone, Default)]
262pub struct UserInputEvent {
263 pub text: String,
264}
265
266#[derive(Debug, Clone, Default)]
267pub struct UserInputResult {
268 pub handled: bool,
270 pub text: Option<String>,
272 pub reason: String,
273}
274
275#[derive(Debug, Clone, Default)]
276pub struct TurnStoppingEvent {
277 pub turn_index: u32,
278}
279
280#[derive(Debug, Clone, Default)]
281pub struct TurnStoppingResult {
282 pub continue_: bool,
284 pub message: String,
286 pub reason: String,
287}
288
289#[derive(Default)]
293struct Handlers {
294 tool_call: Option<Box<dyn FnMut(ToolCallEvent) -> Option<ToolCallResult>>>,
295 tool_result: Option<Box<dyn FnMut(ToolResultEvent) -> Option<ToolResultResult>>>,
296 before_agent_start:
297 Option<Box<dyn FnMut(BeforeAgentStartEvent) -> Option<BeforeAgentStartResult>>>,
298 session_before_switch:
299 Option<Box<dyn FnMut(SessionBeforeSwitchEvent) -> Option<SessionBeforeSwitchResult>>>,
300 user_input: Option<Box<dyn FnMut(UserInputEvent) -> Option<UserInputResult>>>,
301 turn_stopping: Option<Box<dyn FnMut(TurnStoppingEvent) -> Option<TurnStoppingResult>>>,
302 events: EventHandlers,
303}
304
305pub struct Extension {
307 name: String,
308 version: String,
309 tools: Vec<Tool>,
310 commands: Vec<(String, Command)>,
311 events: Vec<pxb::Event>,
312 intercept: Vec<pxb::Event>,
313 handlers: Handlers,
314}
315
316impl Extension {
317 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
320 Self {
321 name: name.into(),
322 version: version.into(),
323 tools: Vec::new(),
324 commands: Vec::new(),
325 events: Vec::new(),
326 intercept: Vec::new(),
327 handlers: Handlers::default(),
328 }
329 }
330
331 pub fn register_tool(&mut self, tool: Tool) {
333 if !tool.name.is_empty() {
334 self.tools.push(tool);
335 }
336 }
337
338 pub fn register_command(&mut self, name: impl Into<String>, cmd: Command) {
340 let name = name.into();
341 if !name.is_empty() && !self.commands.iter().any(|(n, _)| *n == name) {
342 self.commands.push((name, cmd));
343 }
344 }
345
346 pub fn on_tool_call(
348 &mut self,
349 f: impl FnMut(ToolCallEvent) -> Option<ToolCallResult> + 'static,
350 ) {
351 self.handlers.tool_call = Some(Box::new(f));
352 push_unique(&mut self.intercept, pxb::Event::ToolCall);
353 }
354
355 pub fn on_tool_result(
357 &mut self,
358 f: impl FnMut(ToolResultEvent) -> Option<ToolResultResult> + 'static,
359 ) {
360 self.handlers.tool_result = Some(Box::new(f));
361 push_unique(&mut self.intercept, pxb::Event::ToolResult);
362 }
363
364 pub fn on_before_agent_start(
366 &mut self,
367 f: impl FnMut(BeforeAgentStartEvent) -> Option<BeforeAgentStartResult> + 'static,
368 ) {
369 self.handlers.before_agent_start = Some(Box::new(f));
370 push_unique(&mut self.intercept, pxb::Event::BeforeAgentStart);
371 }
372
373 pub fn on_session_before_switch(
375 &mut self,
376 f: impl FnMut(SessionBeforeSwitchEvent) -> Option<SessionBeforeSwitchResult> + 'static,
377 ) {
378 self.handlers.session_before_switch = Some(Box::new(f));
379 push_unique(&mut self.intercept, pxb::Event::SessionBeforeSwitch);
380 }
381
382 pub fn on_user_input(
384 &mut self,
385 f: impl FnMut(UserInputEvent) -> Option<UserInputResult> + 'static,
386 ) {
387 self.handlers.user_input = Some(Box::new(f));
388 push_unique(&mut self.intercept, pxb::Event::UserInput);
389 }
390
391 pub fn on_turn_stopping(
393 &mut self,
394 f: impl FnMut(TurnStoppingEvent) -> Option<TurnStoppingResult> + 'static,
395 ) {
396 self.handlers.turn_stopping = Some(Box::new(f));
397 push_unique(&mut self.intercept, pxb::Event::TurnStopping);
398 }
399
400 pub fn subscribe(&mut self, event: pxb::Event, f: impl FnMut(pxb::EventNotify) + 'static) {
403 let code = event.code();
404 if code == 0 {
405 return;
406 }
407 push_unique(&mut self.events, event);
408 self.handlers.events.insert(code, Box::new(f));
409 }
410
411 pub fn run(self) -> Result<(), Error> {
413 let stdin = io::stdin();
414 let stdout = io::stdout();
415 let mut rd = stdin.lock();
416 let mut wr = stdout.lock();
417
418 let rt = tokio::runtime::Builder::new_current_thread().build()?;
421
422 let host = handshake(&mut rd, &mut wr, &self)?;
423 register(&mut wr, &self)?;
424
425 let Extension {
428 tools,
429 commands,
430 handlers,
431 ..
432 } = self;
433 serve(&mut rd, &mut wr, host, tools, commands, handlers, &rt)
434 }
435}
436
437fn handshake(rd: &mut Rd, wr: &mut Wr, ext: &Extension) -> Result<HostInfo, Error> {
439 let mut caps = 0u32;
440 if !ext.commands.is_empty() {
441 caps |= pxb::CAP_COMMANDS;
442 }
443 if !ext.tools.is_empty() {
444 caps |= pxb::CAP_TOOLS;
445 }
446 if !ext.events.is_empty() {
447 caps |= pxb::CAP_EVENTS;
448 }
449 if !ext.intercept.is_empty() {
450 caps |= pxb::CAP_INTERCEPT;
451 }
452
453 let hello = pxb::encode_hello(&pxb::Hello {
454 name: ext.name.clone(),
455 version: ext.version.clone(),
456 caps,
457 protocol: pxb::PROTOCOL_VERSION,
458 });
459 pxb::write_frame(wr, pxb::TYPE_HELLO, 0, 0, &hello)?;
460
461 let f = pxb::read_frame(rd)?;
462 if f.header.typ != pxb::TYPE_HELLO_ACK {
463 return Err(Error::UnexpectedFrame {
464 want: "hello_ack",
465 got: f.header.typ,
466 });
467 }
468 let ack = pxb::decode_hello_ack(&f.body)?;
469 Ok(HostInfo {
470 cwd: ack.cwd,
471 session_id: ack.session_id,
472 extension_dir: ack.extension_dir,
473 phi_version: ack.phi_version,
474 })
475}
476
477fn register(wr: &mut Wr, ext: &Extension) -> Result<(), Error> {
479 for tool in &ext.tools {
480 let body = pxb::encode_register_tool(&pxb::RegisterTool {
481 name: tool.name.clone(),
482 description: tool.description.clone(),
483 schema_json: tool.schema.to_json_bytes(),
484 timeout_sec: tool.timeout_sec,
485 has_detail: tool.detail_from_args.is_some(),
486 readable: tool.readable,
487 });
488 pxb::write_frame(wr, pxb::TYPE_REGISTER_TOOL, 0, 0, &body)?;
489 }
490 for (name, cmd) in &ext.commands {
491 let body = pxb::encode_register_command(&pxb::RegisterCommand {
492 name: name.clone(),
493 description: cmd.description.clone(),
494 needs_args: cmd.needs_args,
495 });
496 pxb::write_frame(wr, pxb::TYPE_REGISTER_COMMAND, 0, 0, &body)?;
497 }
498 if !ext.events.is_empty() || !ext.intercept.is_empty() {
499 let body = pxb::encode_subscribe(&pxb::Subscribe {
500 events: ext.events.iter().map(|e| e.code()).collect(),
501 intercept: ext.intercept.iter().map(|e| e.code()).collect(),
502 });
503 pxb::write_frame(wr, pxb::TYPE_SUBSCRIBE, 0, 0, &body)?;
504 }
505 pxb::write_frame(wr, pxb::TYPE_READY, 0, 0, &[])
506}
507
508fn serve(
511 rd: &mut Rd,
512 wr: &mut Wr,
513 mut host: HostInfo,
514 mut tools: Vec<Tool>,
515 mut commands: Vec<(String, Command)>,
516 mut handlers: Handlers,
517 rt: &tokio::runtime::Runtime,
518) -> Result<(), Error> {
519 let mut pending_submit: Option<String> = None;
520 let mut next_host_id: u32 = 0;
521
522 loop {
523 let f = pxb::read_frame(rd)?;
524 match pxb::FrameType::from_u16(f.header.typ) {
525 pxb::FrameType::Shutdown => {
526 pxb::write_frame(wr, pxb::TYPE_SHUTDOWN_ACK, 0, 0, &[])?;
527 return Ok(());
528 }
529 pxb::FrameType::CommandInvoked => serve_command(
530 rd,
531 wr,
532 &f,
533 &mut host,
534 &mut commands,
535 &mut handlers.events,
536 &mut pending_submit,
537 &mut next_host_id,
538 )?,
539 pxb::FrameType::ToolInvoke => serve_tool(wr, &f, &mut tools, rt)?,
540 pxb::FrameType::ToolDetailInvoke => serve_tool_detail(wr, &f, &mut tools)?,
541 pxb::FrameType::Intercept => serve_intercept(wr, &f, &mut handlers)?,
542 pxb::FrameType::Event => {
543 if let Ok(ev) = pxb::decode_event_notify(&f.body) {
544 dispatch_event(&mut handlers.events, ev);
545 }
546 }
547 pxb::FrameType::SessionMeta => {
548 if let Ok(meta) = pxb::decode_session_meta(&f.body) {
549 apply_session_meta(&mut host, meta);
550 }
551 }
552 _ => {}
554 }
555 }
556}
557
558#[allow(clippy::too_many_arguments)] fn serve_command(
566 rd: &mut Rd,
567 wr: &mut Wr,
568 frame: &pxb::Frame,
569 host: &mut HostInfo,
570 commands: &mut [(String, Command)],
571 events: &mut EventHandlers,
572 pending_submit: &mut Option<String>,
573 next_host_id: &mut u32,
574) -> Result<(), Error> {
575 let inv = pxb::decode_command_invoked(&frame.body)?;
576 let mut resp = pxb::CommandResponse {
577 ok: true,
578 ..Default::default()
579 };
580 if let Some((_, cmd)) = commands.iter_mut().find(|(n, _)| *n == inv.name) {
581 let mut ctx = Context {
582 cwd: host.cwd.clone(),
583 session_id: host.session_id.clone(),
584 has_ui: true,
585 rd,
586 wr,
587 host,
588 pending_submit,
589 next_host_id,
590 events,
591 };
592 if let Err(e) = (cmd.handler)(&inv.args, &mut ctx) {
593 resp.ok = false;
594 resp.error = e;
595 }
596 } else {
597 resp.ok = false;
598 resp.error = "unknown command".into();
599 }
600 resp.submit = pending_submit.take().unwrap_or_default();
601 let body = pxb::encode_command_response(&resp);
602 pxb::write_frame(
603 wr,
604 pxb::TYPE_COMMAND_RESPONSE,
605 frame.header.flags,
606 frame.header.id,
607 &body,
608 )?;
609 Ok(())
610}
611
612fn serve_tool(
617 wr: &mut Wr,
618 frame: &pxb::Frame,
619 tools: &mut [Tool],
620 rt: &tokio::runtime::Runtime,
621) -> Result<(), Error> {
622 let inv = pxb::decode_tool_invoke(&frame.body)?;
623 let tr = match tools.iter_mut().find(|t| t.name == inv.name) {
624 Some(tool) => match rt.block_on((tool.execute)(&inv.args)) {
625 Ok(res) => pxb::ToolResultMsg {
626 content: res.content,
627 detail: res.detail,
628 output: res.output,
629 ..Default::default()
630 },
631 Err(e) => tool_error(e),
632 },
633 None => tool_error("unknown tool"),
634 };
635 let body = pxb::encode_tool_result(&tr);
636 pxb::write_frame(
637 wr,
638 pxb::TYPE_TOOL_RESULT,
639 frame.header.flags,
640 frame.header.id,
641 &body,
642 )?;
643 Ok(())
644}
645
646fn serve_tool_detail(wr: &mut Wr, frame: &pxb::Frame, tools: &mut [Tool]) -> Result<(), Error> {
648 let inv = pxb::decode_tool_invoke(&frame.body)?;
649 let detail = tools
650 .iter_mut()
651 .find(|t| t.name == inv.name)
652 .and_then(|t| t.detail_from_args.as_mut())
653 .map(|f| f(&inv.args))
654 .unwrap_or_default();
655 let body = pxb::encode_tool_detail_result(&pxb::ToolDetailResult { detail });
656 pxb::write_frame(
657 wr,
658 pxb::TYPE_TOOL_DETAIL_RESULT,
659 frame.header.flags,
660 frame.header.id,
661 &body,
662 )?;
663 Ok(())
664}
665
666fn serve_intercept(wr: &mut Wr, frame: &pxb::Frame, handlers: &mut Handlers) -> Result<(), Error> {
668 let req = pxb::decode_intercept_req(&frame.body)?;
669 let resp = handle_intercept(req, handlers);
670 let body = pxb::encode_intercept_resp(&resp);
671 pxb::write_frame(
672 wr,
673 pxb::TYPE_INTERCEPT_RESPONSE,
674 frame.header.flags,
675 frame.header.id,
676 &body,
677 )?;
678 Ok(())
679}
680
681fn handle_intercept(req: pxb::InterceptReq, handlers: &mut Handlers) -> pxb::InterceptResp {
685 let mut resp = pxb::InterceptResp::default();
686 match pxb::Event::from_code(req.event) {
687 pxb::Event::ToolCall => {
688 let Some(f) = handlers.tool_call.as_mut() else {
689 return resp;
690 };
691 let Some(r) = f(ToolCallEvent {
692 tool_name: req.tool_name,
693 tool_call_id: req.tool_call_id,
694 input: req.input,
695 }) else {
696 return resp;
697 };
698 resp.block = r.block;
699 resp.reason = r.reason;
700 resp.context = r.context;
701 if let Some(v) = r.input {
702 resp.input = v;
703 }
704 }
705 pxb::Event::ToolResult => {
706 let Some(f) = handlers.tool_result.as_mut() else {
707 return resp;
708 };
709 let Some(r) = f(ToolResultEvent {
710 tool_name: req.tool_name,
711 tool_call_id: req.tool_call_id,
712 input: req.input,
713 content: req.content,
714 is_error: req.is_error,
715 err: req.err_text,
716 }) else {
717 return resp;
718 };
719 resp.context = r.context;
720 resp.stop = r.stop;
721 resp.reason = r.reason;
722 if let Some(v) = r.content {
723 resp.content = v;
724 }
725 }
726 pxb::Event::BeforeAgentStart => {
727 let Some(f) = handlers.before_agent_start.as_mut() else {
728 return resp;
729 };
730 let Some(r) = f(BeforeAgentStartEvent { prompt: req.prompt }) else {
731 return resp;
732 };
733 resp.system_prompt_append = r.system_prompt_append;
734 if let Some(v) = r.prompt {
735 resp.prompt = v;
736 }
737 }
738 pxb::Event::SessionBeforeSwitch => {
739 let Some(f) = handlers.session_before_switch.as_mut() else {
740 return resp;
741 };
742 let Some(r) = f(SessionBeforeSwitchEvent {
743 reason: req.reason,
744 target_session_id: req.target_id,
745 }) else {
746 return resp;
747 };
748 resp.cancel = r.cancel;
749 resp.reason = r.reason;
750 resp.toast = r.toast;
751 }
752 pxb::Event::UserInput => {
753 let Some(f) = handlers.user_input.as_mut() else {
754 return resp;
755 };
756 let Some(r) = f(UserInputEvent { text: req.prompt }) else {
757 return resp;
758 };
759 resp.handled = r.handled;
760 resp.reason = r.reason;
761 if let Some(v) = r.text {
762 resp.prompt = v;
763 }
764 }
765 pxb::Event::TurnStopping => {
766 let Some(f) = handlers.turn_stopping.as_mut() else {
767 return resp;
768 };
769 let Some(r) = f(TurnStoppingEvent {
770 turn_index: req.turn_index,
771 }) else {
772 return resp;
773 };
774 resp.continue_ = r.continue_;
775 resp.prompt = r.message;
776 resp.reason = r.reason;
777 }
778 _ => {}
779 }
780 resp
781}
782
783fn apply_session_meta(host: &mut HostInfo, meta: pxb::SessionMeta) {
785 if !meta.session_id.is_empty() {
786 host.session_id = meta.session_id;
787 }
788 if !meta.cwd.is_empty() {
789 host.cwd = meta.cwd;
790 }
791}
792
793fn dispatch_event(handlers: &mut EventHandlers, ev: pxb::EventNotify) {
795 if let Some(handler) = handlers.get_mut(&ev.event) {
796 handler(ev);
797 }
798}
799
800fn tool_error(message: impl Into<String>) -> pxb::ToolResultMsg {
803 let message = message.into();
804 pxb::ToolResultMsg {
805 is_error: true,
806 error: message.clone(),
807 content: message,
808 ..Default::default()
809 }
810}
811
812pub struct Context<'a> {
816 pub cwd: String,
817 pub session_id: String,
818 pub has_ui: bool,
819 rd: &'a mut Rd,
820 wr: &'a mut Wr,
821 host: &'a mut HostInfo,
822 pending_submit: &'a mut Option<String>,
823 next_host_id: &'a mut u32,
824 events: &'a mut EventHandlers,
825}
826
827impl Context<'_> {
828 pub fn notify(&mut self, level: &str, message: &str) {
830 let body = pxb::encode_notify(&pxb::NotifyMsg {
831 level: level.into(),
832 message: message.into(),
833 ..Default::default()
834 });
835 let _ = pxb::write_frame(self.wr, pxb::TYPE_NOTIFY, 0, 0, &body);
836 }
837
838 pub fn set_status(&mut self, text: &str) {
840 let body = pxb::encode_notify(&pxb::NotifyMsg {
841 status: text.into(),
842 status_set: true,
843 ..Default::default()
844 });
845 let _ = pxb::write_frame(self.wr, pxb::TYPE_NOTIFY, 0, 0, &body);
846 }
847
848 pub fn submit(&mut self, text: &str) {
851 *self.pending_submit = Some(text.to_string());
852 }
853
854 pub fn send_user_message(&mut self, text: &str) {
857 if text.is_empty() {
858 return;
859 }
860 let body = pxb::encode_host_request(&pxb::HostRequest {
861 method: "send_user_message".into(),
862 arg: text.into(),
863 });
864 let _ = pxb::write_frame(self.wr, pxb::TYPE_HOST_REQUEST, 0, 0, &body);
865 }
866
867 pub fn confirm(&mut self, title: &str, message: &str) -> ConfirmReply {
869 self.confirm_opts(ConfirmRequest {
870 title: title.into(),
871 message: message.into(),
872 ..Default::default()
873 })
874 }
875
876 pub fn confirm_opts(&mut self, req: ConfirmRequest) -> ConfirmReply {
878 let Some(id) = self.send_host_request("confirm", &confirm_request_json(&req)) else {
879 return ConfirmReply::default();
880 };
881 loop {
884 let Ok(f) = pxb::read_frame(self.rd) else {
885 return ConfirmReply::default();
886 };
887 if let Some(reply) = self.nested_reply(f, id) {
888 return reply;
889 }
890 }
891 }
892
893 fn send_host_request(&mut self, method: &str, arg: &str) -> Option<u32> {
896 *self.next_host_id = self.next_host_id.wrapping_add(1);
897 let id = *self.next_host_id;
898 let body = pxb::encode_host_request(&pxb::HostRequest {
899 method: method.into(),
900 arg: arg.into(),
901 });
902 if pxb::write_frame(self.wr, pxb::TYPE_HOST_REQUEST, pxb::FLAG_HAS_ID, id, &body).is_err() {
903 return None;
904 }
905 Some(id)
906 }
907
908 fn nested_reply(&mut self, f: pxb::Frame, want_id: u32) -> Option<ConfirmReply> {
912 match pxb::FrameType::from_u16(f.header.typ) {
913 pxb::FrameType::HostResult => {
914 if f.header.flags & pxb::FLAG_HAS_ID == 0 || f.header.id != want_id {
915 return None;
916 }
917 let Ok(res) = pxb::decode_host_result(&f.body) else {
918 return Some(ConfirmReply::default());
919 };
920 Some(ConfirmReply { ok: res.ok })
921 }
922 pxb::FrameType::SessionMeta => {
923 if let Ok(meta) = pxb::decode_session_meta(&f.body) {
924 apply_session_meta(self.host, meta);
925 }
926 None
927 }
928 pxb::FrameType::Event => {
929 if let Ok(ev) = pxb::decode_event_notify(&f.body) {
930 dispatch_event(self.events, ev);
931 }
932 None
933 }
934 pxb::FrameType::Shutdown => {
935 let _ = pxb::write_frame(self.wr, pxb::TYPE_SHUTDOWN_ACK, 0, 0, &[]);
936 Some(ConfirmReply::default())
937 }
938 _ => None,
939 }
940 }
941}
942
943fn confirm_request_json(req: &ConfirmRequest) -> String {
948 serde_json::to_string(req)
949 .expect("ConfirmRequest holds only strings/bool; serialization cannot fail")
950}
951
952fn push_unique(xs: &mut Vec<pxb::Event>, v: pxb::Event) {
953 if !xs.contains(&v) {
954 xs.push(v);
955 }
956}
957
958#[cfg(test)]
959mod tests {
960 use super::*;
961
962 #[test]
963 fn confirm_json_matches_go_field_names() {
964 let req = ConfirmRequest {
965 title: "Delete?".into(),
966 message: "Remove /tmp/x".into(),
967 yes: "Delete".into(),
968 no: "Cancel".into(),
969 danger: true,
970 };
971 assert_eq!(
972 confirm_request_json(&req),
973 r#"{"Title":"Delete?","Message":"Remove /tmp/x","Yes":"Delete","No":"Cancel","Danger":true}"#
974 );
975 }
976
977 #[test]
978 fn confirm_json_escapes_quotes_and_controls() {
979 let req = ConfirmRequest {
980 title: "say \"hi\"\n".into(),
981 ..Default::default()
982 };
983 assert_eq!(
984 confirm_request_json(&req),
985 r#"{"Title":"say \"hi\"\n","Message":"","Yes":"","No":"","Danger":false}"#
986 );
987 }
988
989 #[test]
990 fn push_unique_keeps_first() {
991 let mut xs = Vec::new();
992 push_unique(&mut xs, pxb::Event::ToolCall);
993 push_unique(&mut xs, pxb::Event::ToolCall);
994 push_unique(&mut xs, pxb::Event::AgentEnd);
995 assert_eq!(xs, vec![pxb::Event::ToolCall, pxb::Event::AgentEnd]);
996 }
997}