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