1#![cfg_attr(test, allow(clippy::expect_used))]
2
3use motosan_agent_tool::ToolResult;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Debug, Clone, Serialize)]
8pub enum ProgressChunk {
9 Stdout(Vec<u8>),
10 Stderr(Vec<u8>),
11 Status(String),
12}
13
14impl From<crate::tools::ToolProgressChunk> for ProgressChunk {
15 fn from(c: crate::tools::ToolProgressChunk) -> Self {
16 use crate::tools::ToolProgressChunk as TPC;
17 match c {
18 TPC::Stdout(b) => Self::Stdout(b),
19 TPC::Stderr(b) => Self::Stderr(b),
20 TPC::Status(s) => Self::Status(s),
21 }
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct PermissionResolution {
29 pub tool: String,
30 pub args: serde_json::Value,
31 pub choice: PermissionChoice,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum PermissionChoice {
37 AllowOnce,
38 AllowSession,
39 Deny,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
44pub enum Command {
45 SendUserMessage {
46 text: String,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
48 attachments: Vec<crate::user_message::Attachment>,
49 },
50 CancelAgent,
51 Quit,
52 ResolvePermission(PermissionResolution),
53 SetPermissionMode {
57 mode: crate::permissions::PermissionMode,
58 },
59 RunInlineBash {
63 command: String,
64 send_to_llm: bool,
65 },
66 Compact,
68 ApplySettings {
75 settings: crate::settings::Settings,
76 },
77 ReloadSettings,
82 NewSession,
84 CloneSession,
87 GetMessages,
90 GetState,
93 ListExtensions,
96 InvokeExtensionCommand {
100 name: String,
101 args: String,
102 },
103 SwitchModel(crate::model::ModelId),
105 LoadSession(String),
107 ForkFrom {
109 from: String,
110 message: String,
111 },
112}
113
114impl PartialEq for Command {
115 fn eq(&self, other: &Self) -> bool {
116 use Command as C;
117 match (self, other) {
118 (
119 C::SendUserMessage {
120 text: left_text,
121 attachments: left_attachments,
122 },
123 C::SendUserMessage {
124 text: right_text,
125 attachments: right_attachments,
126 },
127 ) => left_text == right_text && left_attachments == right_attachments,
128 (C::CancelAgent, C::CancelAgent)
129 | (C::Quit, C::Quit)
130 | (C::Compact, C::Compact)
131 | (C::ReloadSettings, C::ReloadSettings)
132 | (C::NewSession, C::NewSession)
133 | (C::CloneSession, C::CloneSession)
134 | (C::GetMessages, C::GetMessages)
135 | (C::GetState, C::GetState)
136 | (C::ListExtensions, C::ListExtensions) => true,
137 (C::ResolvePermission(left), C::ResolvePermission(right)) => left == right,
138 (C::SetPermissionMode { mode: left }, C::SetPermissionMode { mode: right }) => {
139 left == right
140 }
141 (
142 C::RunInlineBash {
143 command: left_command,
144 send_to_llm: left_send_to_llm,
145 },
146 C::RunInlineBash {
147 command: right_command,
148 send_to_llm: right_send_to_llm,
149 },
150 ) => left_command == right_command && left_send_to_llm == right_send_to_llm,
151 (C::ApplySettings { settings: left }, C::ApplySettings { settings: right }) => {
152 settings_command_eq(left, right)
153 }
154 (
155 C::InvokeExtensionCommand {
156 name: left_name,
157 args: left_args,
158 },
159 C::InvokeExtensionCommand {
160 name: right_name,
161 args: right_args,
162 },
163 ) => left_name == right_name && left_args == right_args,
164 (C::SwitchModel(left), C::SwitchModel(right)) => left == right,
165 (C::LoadSession(left), C::LoadSession(right)) => left == right,
166 (
167 C::ForkFrom {
168 from: left_from,
169 message: left_message,
170 },
171 C::ForkFrom {
172 from: right_from,
173 message: right_message,
174 },
175 ) => left_from == right_from && left_message == right_message,
176 _ => false,
177 }
178 }
179}
180
181impl Eq for Command {}
182
183fn settings_command_eq(
184 left: &crate::settings::Settings,
185 right: &crate::settings::Settings,
186) -> bool {
187 left.model == right.model
188 && left.anthropic == right.anthropic
189 && left.ui == right.ui
190 && left.session.autosave == right.session.autosave
191 && left.session.compact_at_context_pct.to_bits()
192 == right.session.compact_at_context_pct.to_bits()
193 && left.session.max_context_tokens == right.session.max_context_tokens
194 && left.session.keep_turns == right.session.keep_turns
195 && left.logging == right.logging
196 && left.permissions == right.permissions
197}
198
199#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
200pub struct ExtensionInfo {
201 pub name: String,
202 pub hooks: Vec<String>,
203 pub commands: Vec<String>,
204 pub healthy: bool,
207 pub diagnostic: Option<String>,
209}
210
211#[derive(Debug, Serialize)]
212#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
213pub enum UiEvent {
214 AgentTurnStarted,
215 AgentThinking,
216 AgentTextDelta(String),
217 AgentThinkingDelta(String),
226 ThinkingComplete {
251 text: String,
252 },
253 AgentMessageComplete(String),
254 ToolCallStarted {
255 id: String,
256 name: String,
257 args: Value,
258 },
259 ToolCallProgress {
260 id: String,
261 chunk: ProgressChunk,
262 },
263 ToolCallCompleted {
264 id: String,
265 result: UiToolResult,
266 },
267 AgentTurnComplete,
268 TurnStats {
274 input_tokens: u64,
275 output_tokens: u64,
276 cumulative_input: u64,
277 cumulative_output: u64,
278 model: String,
279 },
280 PermissionRequested {
281 tool: String,
282 args: serde_json::Value,
283 #[serde(skip)]
284 resolver: tokio::sync::oneshot::Sender<crate::permissions::Decision>,
285 },
286 PermissionModeChanged {
290 mode: crate::permissions::PermissionMode,
291 },
292 InlineBashOutput {
294 command: String,
295 output: String,
296 },
297 SessionReplaced {
304 session_id: String,
305 history: Vec<motosan_agent_loop::Message>,
306 },
307 ModelSwitched(crate::model::ModelId),
309 ForkCandidates(Vec<(String, String)>),
313 BranchTree(
316 #[serde(serialize_with = "crate::protocol::serialize_branch_tree")]
317 motosan_agent_loop::BranchTree,
318 ),
319 MessagesSnapshot(Vec<motosan_agent_loop::Message>),
322 StateSnapshot {
325 session_id: String,
326 model: String,
327 active_turn: bool,
328 },
329 ExtensionList {
332 extensions: Vec<ExtensionInfo>,
333 },
334 SettingsSnapshot {
339 settings: crate::settings::Settings,
340 },
341 Error(String),
342 AttachmentError {
346 kind: crate::user_message::AttachmentErrorKind,
347 message: String,
348 },
349 ExtensionCancelled {
354 extension_name: String,
355 reason: Option<String>,
356 },
357 Notice {
360 title: String,
361 body: String,
362 },
363}
364
365#[derive(Debug, Clone, Serialize)]
366pub struct UiToolResult {
367 pub is_error: bool,
368 pub text: String,
369}
370
371impl From<&ToolResult> for UiToolResult {
372 fn from(r: &ToolResult) -> Self {
373 Self {
374 is_error: r.is_error,
375 text: format!("{r:?}"),
376 }
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn attachment_error_event_serializes_with_kind_and_message() {
386 use crate::user_message::AttachmentErrorKind;
387
388 let ev = UiEvent::AttachmentError {
389 kind: AttachmentErrorKind::NotFound,
390 message: "image not found: /tmp/foo.png".into(),
391 };
392 let json = serde_json::to_string(&ev).expect("serialize");
393 assert_eq!(
395 json,
396 r#"{"type":"attachment_error","payload":{"kind":"not_found","message":"image not found: /tmp/foo.png"}}"#
397 );
398 }
399
400 #[test]
401 fn compact_command_is_constructible() {
402 let c = Command::Compact;
403 assert_eq!(c, Command::Compact);
404 assert!(format!("{c:?}").contains("Compact"));
405 }
406
407 #[test]
408 fn d2_command_and_event_variants_construct() {
409 let _ = Command::NewSession;
410 let _ = Command::SwitchModel(crate::model::ModelId::from("m"));
411 let _ = Command::LoadSession("sess-id".into());
412 let e = UiEvent::SessionReplaced {
413 session_id: "test".into(),
414 history: Vec::new(),
415 };
416 assert!(format!("{e:?}").contains("SessionReplaced"));
417 let e = UiEvent::ModelSwitched(crate::model::ModelId::from("m"));
418 assert!(format!("{e:?}").contains("ModelSwitched"));
419 }
420
421 #[test]
422 fn clone_session_command_constructs() {
423 let c = Command::CloneSession;
424 assert!(format!("{c:?}").contains("CloneSession"));
425 }
426
427 #[test]
428 fn read_query_commands_round_trip() {
429 for c in [Command::GetMessages, Command::GetState] {
430 let line = serde_json::to_string(&c).expect("serialize");
431 assert!(line.contains("\"type\":"), "{line}");
432 let back: Command = serde_json::from_str(&line).expect("deserialize");
433 assert_eq!(c, back);
434 }
435 assert_eq!(
437 serde_json::to_string(&Command::GetMessages).expect("ser"),
438 r#"{"type":"get_messages"}"#
439 );
440 }
441
442 #[test]
443 fn fork_protocol_variants_construct() {
444 let c = Command::ForkFrom {
445 from: "e1".into(),
446 message: "hi".into(),
447 };
448 assert!(format!("{c:?}").contains("ForkFrom"));
449 let e = UiEvent::ForkCandidates(vec![("e1".into(), "first".into())]);
450 assert!(format!("{e:?}").contains("ForkCandidates"));
451 }
452
453 #[test]
454 fn branch_tree_event_constructs() {
455 let tree = motosan_agent_loop::BranchTree {
456 nodes: Vec::new(),
457 root: None,
458 active_leaf: None,
459 };
460 let e = UiEvent::BranchTree(tree);
461 assert!(format!("{e:?}").contains("BranchTree"));
462 }
463
464 #[test]
465 fn run_inline_bash_command_is_constructible() {
466 let c = Command::RunInlineBash {
467 command: "ls".into(),
468 send_to_llm: true,
469 };
470 assert!(format!("{c:?}").contains("RunInlineBash"));
471 }
472
473 #[test]
474 fn permission_protocol_variants_are_constructible() {
475 let command = Command::ResolvePermission(PermissionResolution {
476 tool: "bash".into(),
477 args: serde_json::json!({"command": "echo hi"}),
478 choice: PermissionChoice::AllowSession,
479 });
480 assert!(format!("{command:?}").contains("ResolvePermission"));
481 assert!(format!("{command:?}").contains("AllowSession"));
482
483 let (resolver, _rx) = tokio::sync::oneshot::channel::<crate::permissions::Decision>();
484 let event = UiEvent::PermissionRequested {
485 tool: "bash".into(),
486 args: serde_json::json!({"command": "echo hi"}),
487 resolver,
488 };
489 assert!(format!("{event:?}").contains("PermissionRequested"));
490 }
491
492 #[test]
493 fn command_round_trips_through_json() {
494 let cases = vec![
495 Command::SendUserMessage {
496 text: "hi".into(),
497 attachments: Vec::new(),
498 },
499 Command::CancelAgent,
500 Command::Quit,
501 Command::Compact,
502 Command::NewSession,
503 Command::CloneSession,
504 Command::RunInlineBash {
505 command: "ls".into(),
506 send_to_llm: true,
507 },
508 Command::SwitchModel(crate::model::ModelId::from("claude-opus-4-7")),
509 Command::LoadSession("sess-1".into()),
510 Command::ForkFrom {
511 from: "e1".into(),
512 message: "hi".into(),
513 },
514 Command::ResolvePermission(PermissionResolution {
515 tool: "bash".into(),
516 args: serde_json::json!({"command": "echo hi"}),
517 choice: PermissionChoice::AllowSession,
518 }),
519 ];
520 for c in cases {
521 let line = serde_json::to_string(&c).expect("serialize");
522 assert!(line.contains("\"type\":"), "missing type tag: {line}");
523 let back: Command = serde_json::from_str(&line).expect("deserialize");
524 assert_eq!(c, back, "round-trip mismatch for {line}");
525 }
526 }
527
528 #[test]
529 fn command_send_user_message_round_trip() {
530 let line = serde_json::to_string(&Command::SendUserMessage {
531 text: "hi".into(),
532 attachments: Vec::new(),
533 })
534 .expect("serialize");
535 assert_eq!(
536 line,
537 r#"{"type":"send_user_message","payload":{"text":"hi"}}"#
538 );
539 let back: Command = serde_json::from_str(&line).expect("deserialize");
540 assert_eq!(
541 back,
542 Command::SendUserMessage {
543 text: "hi".into(),
544 attachments: Vec::new(),
545 }
546 );
547 }
548
549 #[test]
550 fn command_send_user_message_with_attachment_round_trip() {
551 let cmd = Command::SendUserMessage {
552 text: "look".into(),
553 attachments: vec![crate::user_message::Attachment::Image {
554 path: std::path::PathBuf::from("/tmp/foo.png"),
555 }],
556 };
557 let line = serde_json::to_string(&cmd).expect("serialize");
558 assert_eq!(
559 line,
560 r#"{"type":"send_user_message","payload":{"text":"look","attachments":[{"type":"image","path":"/tmp/foo.png"}]}}"#
561 );
562 let back: Command = serde_json::from_str(&line).expect("deserialize");
563 assert_eq!(back, cmd);
564 }
565
566 #[test]
567 fn unit_command_uses_adjacent_tagging() {
568 let unit = serde_json::to_string(&Command::CancelAgent).expect("serialize");
569 assert_eq!(unit, r#"{"type":"cancel_agent"}"#);
570 }
571
572 #[test]
573 fn command_invoke_extension_command_serde_round_trip() {
574 let cmd = Command::InvokeExtensionCommand {
575 name: "todo".into(),
576 args: "add buy milk".into(),
577 };
578 let json = serde_json::to_string(&cmd).expect("ok");
579 assert_eq!(
580 json,
581 r#"{"type":"invoke_extension_command","payload":{"name":"todo","args":"add buy milk"}}"#
582 );
583 let back: Command = serde_json::from_str(&json).expect("ok");
584 assert_eq!(back, cmd);
585 }
586
587 #[test]
588 fn ui_event_extension_cancelled_serializes() {
589 let ev = UiEvent::ExtensionCancelled {
590 extension_name: "dirty-repo-guard".into(),
591 reason: Some("uncommitted changes".into()),
592 };
593 let json = serde_json::to_string(&ev).expect("ok");
594 assert!(json.contains(r#""type":"extension_cancelled""#));
595 assert!(json.contains(r#""extension_name":"dirty-repo-guard""#));
596 assert!(json.contains(r#""reason":"uncommitted changes""#));
597 }
598
599 #[test]
600 fn ui_event_notice_serializes() {
601 let ev = UiEvent::Notice {
602 title: "/todo".into(),
603 body: "reply from fixture".into(),
604 };
605 let json = serde_json::to_string(&ev).expect("ok");
606 assert!(json.contains(r#""type":"notice""#));
607 assert!(json.contains(r#""title":"/todo""#));
608 assert!(json.contains(r#""body":"reply from fixture""#));
609 }
610
611 #[test]
612 fn ui_event_thinking_complete_serializes() {
613 let ev = UiEvent::ThinkingComplete {
614 text: "the model considered options A and B".into(),
615 };
616 let line = serde_json::to_string(&ev).expect("serialize");
617 assert_eq!(
618 line,
619 r#"{"type":"thinking_complete","payload":{"text":"the model considered options A and B"}}"#
620 );
621 }
622
623 #[test]
624 fn ui_event_thinking_delta_serializes() {
625 let ev = UiEvent::AgentThinkingDelta("partial thought ".into());
626 let json = serde_json::to_string(&ev).expect("serialize");
627 assert_eq!(
628 json,
629 r#"{"type":"agent_thinking_delta","payload":"partial thought "}"#
630 );
631 }
632
633 #[test]
634 fn ui_event_session_replaced_new_payload_serializes() {
635 let ev = UiEvent::SessionReplaced {
636 session_id: "01HK1234".into(),
637 history: Vec::new(),
638 };
639 let line = serde_json::to_string(&ev).expect("serialize");
640 assert!(line.contains(r#""type":"session_replaced""#));
641 assert!(line.contains(r#""session_id":"01HK1234""#));
642 assert!(line.contains(r#""history":[]"#));
643 }
644
645 #[test]
646 fn ui_event_turn_stats_round_trips() {
647 let ev = UiEvent::TurnStats {
648 input_tokens: 1203,
649 output_tokens: 412,
650 cumulative_input: 12543,
651 cumulative_output: 5231,
652 model: "claude-opus-4-7".into(),
653 };
654 let line = serde_json::to_string(&ev).expect("serialize");
655 assert_eq!(
656 line,
657 r#"{"type":"turn_stats","payload":{"input_tokens":1203,"output_tokens":412,"cumulative_input":12543,"cumulative_output":5231,"model":"claude-opus-4-7"}}"#
658 );
659 }
660
661 #[test]
662 fn ui_event_permission_mode_changed_serializes() {
663 use crate::permissions::PermissionMode;
664 let ev = UiEvent::PermissionModeChanged {
665 mode: PermissionMode::Bypass,
666 };
667 let line = serde_json::to_string(&ev).expect("serialize");
668 assert_eq!(
669 line,
670 r#"{"type":"permission_mode_changed","payload":{"mode":"bypass"}}"#
671 );
672 }
674
675 #[test]
676 fn command_set_permission_mode_round_trips() {
677 use crate::permissions::PermissionMode;
678 let cmd = Command::SetPermissionMode {
679 mode: PermissionMode::AcceptEdits,
680 };
681 let line = serde_json::to_string(&cmd).expect("serialize");
682 assert_eq!(
683 line,
684 r#"{"type":"set_permission_mode","payload":{"mode":"accept-edits"}}"#
685 );
686 let back: Command = serde_json::from_str(&line).expect("deserialize");
687 assert_eq!(back, cmd);
688 }
689
690 #[test]
691 fn command_still_exposes_eq_for_downstream_matchers() {
692 fn assert_eq_impl<T: Eq>() {}
693 assert_eq_impl::<Command>();
694 }
695
696 #[test]
697 fn command_apply_settings_round_trips() {
698 use crate::settings::Settings;
699 let cmd = Command::ApplySettings {
700 settings: Settings::default(),
701 };
702 let line = serde_json::to_string(&cmd).expect("serialize");
703 assert!(line.contains(r#""type":"apply_settings""#));
705 assert!(line.contains(r#""payload":{"#));
706 assert!(line.contains(r#""model":"#));
707 let back: Command = serde_json::from_str(&line).expect("deserialize");
708 assert_eq!(back, cmd);
709 }
710
711 #[test]
712 fn command_reload_settings_round_trips() {
713 let cmd = Command::ReloadSettings;
714 let line = serde_json::to_string(&cmd).expect("serialize");
715 assert_eq!(line, r#"{"type":"reload_settings"}"#);
716 let back: Command = serde_json::from_str(&line).expect("deserialize");
717 assert_eq!(back, cmd);
718 }
719
720 #[test]
721 fn ui_event_settings_snapshot_serializes() {
722 use crate::settings::Settings;
723 let ev = UiEvent::SettingsSnapshot {
724 settings: Settings::default(),
725 };
726 let line = serde_json::to_string(&ev).expect("serialize");
727 assert!(line.contains(r#""type":"settings_snapshot""#));
728 assert!(line.contains(r#""payload":{"#));
729 assert!(line.contains(r#""model":"#));
730 }
731
732 #[test]
733 fn snapshot_events_serialize_with_type_tag() {
734 let m = UiEvent::MessagesSnapshot(Vec::new());
735 let line = serde_json::to_string(&m).expect("serialize");
736 assert!(line.contains(r#""type":"messages_snapshot""#), "{line}");
737
738 let s = UiEvent::StateSnapshot {
739 session_id: "sess-1".into(),
740 model: "claude-opus-4-7".into(),
741 active_turn: false,
742 };
743 let line = serde_json::to_string(&s).expect("serialize");
744 assert!(line.contains(r#""type":"state_snapshot""#), "{line}");
745 assert!(line.contains(r#""session_id":"sess-1""#), "{line}");
746 assert!(line.contains(r#""active_turn":false"#), "{line}");
747 }
748
749 #[test]
750 fn ui_events_serialize_with_type_tag() {
751 let events = vec![
752 UiEvent::AgentTurnStarted,
753 UiEvent::AgentThinking,
754 UiEvent::AgentTextDelta("hi".into()),
755 UiEvent::AgentMessageComplete("done".into()),
756 UiEvent::AgentTurnComplete,
757 UiEvent::InlineBashOutput {
758 command: "ls".into(),
759 output: "x".into(),
760 },
761 UiEvent::Notice {
762 title: "note".into(),
763 body: "body".into(),
764 },
765 UiEvent::SessionReplaced {
766 session_id: "test".into(),
767 history: Vec::new(),
768 },
769 UiEvent::ModelSwitched(crate::model::ModelId::from("m")),
770 UiEvent::ForkCandidates(vec![("e1".into(), "first".into())]),
771 UiEvent::Error("bad".into()),
772 UiEvent::ToolCallStarted {
773 id: "t1".into(),
774 name: "bash".into(),
775 args: serde_json::json!("ls"),
776 },
777 UiEvent::ToolCallProgress {
778 id: "t1".into(),
779 chunk: ProgressChunk::Status("running".into()),
780 },
781 UiEvent::ToolCallCompleted {
782 id: "t1".into(),
783 result: UiToolResult {
784 is_error: false,
785 text: "ok".into(),
786 },
787 },
788 ];
789 for e in &events {
790 let line = serde_json::to_string(e).expect("serialize");
791 assert!(line.contains("\"type\":"), "missing type tag: {line}");
792 }
793 }
794
795 #[test]
796 fn permission_requested_serializes_without_the_resolver() {
797 let (resolver, _rx) = tokio::sync::oneshot::channel::<crate::permissions::Decision>();
798 let e = UiEvent::PermissionRequested {
799 tool: "bash".into(),
800 args: serde_json::json!({"command": "echo hi"}),
801 resolver,
802 };
803 let line = serde_json::to_string(&e).expect("serialize");
804 assert!(line.contains(r#""type":"permission_requested""#), "{line}");
805 assert!(!line.contains("resolver"), "resolver leaked: {line}");
806 }
807
808 #[test]
809 fn branch_tree_event_serializes_via_mapping() {
810 let tree = motosan_agent_loop::BranchTree {
811 nodes: vec![motosan_agent_loop::BranchNode {
812 id: "n0".into(),
813 parent: None,
814 children: vec![],
815 label: "root".into(),
816 }],
817 root: Some(0),
818 active_leaf: Some(0),
819 };
820 let line = serde_json::to_string(&UiEvent::BranchTree(tree)).expect("serialize");
821 assert!(line.contains(r#""type":"branch_tree""#), "{line}");
822 assert!(line.contains(r#""label":"root""#), "{line}");
823 }
824
825 #[test]
826 fn command_list_extensions_serde_round_trip() {
827 let cmd = Command::ListExtensions;
828 let line = serde_json::to_string(&cmd).expect("serialize");
829 assert_eq!(line, r#"{"type":"list_extensions"}"#);
830 let back: Command = serde_json::from_str(&line).expect("deserialize");
831 assert_eq!(back, cmd);
832 }
833
834 #[test]
835 fn ui_event_extension_list_serializes() {
836 let ev = UiEvent::ExtensionList {
837 extensions: vec![ExtensionInfo {
838 name: "dirty".into(),
839 hooks: vec!["session_before_switch".into()],
840 commands: Vec::new(),
841 healthy: true,
842 diagnostic: None,
843 }],
844 };
845 let line = serde_json::to_string(&ev).expect("serialize");
846 assert!(line.contains(r#""type":"extension_list""#));
847 assert!(line.contains(r#""name":"dirty""#));
848 }
849}