Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:04:41.017Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

events.rs

718 lines · 24.7 KB · rust
1use std::rc::Rc;
2
3use gpui::{
4    Capslock, DispatchEventResult, ExternalPaths, FileDropEvent, KeyDownEvent, KeyUpEvent,
5    Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent,
6    MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformInput, Point, ScrollDelta,
7    ScrollWheelEvent, TouchPhase, point, px,
8};
9use smallvec::smallvec;
10use wasm_bindgen::prelude::*;
11
12use crate::window::WebWindowInner;
13
14pub struct WebEventListeners {
15    #[allow(dead_code)]
16    closures: Vec<Closure<dyn FnMut(JsValue)>>,
17}
18
19pub(crate) struct ClickState {
20    last_position: Point<Pixels>,
21    last_time: f64,
22    current_count: usize,
23}
24
25impl Default for ClickState {
26    fn default() -> Self {
27        Self {
28            last_position: Point::default(),
29            last_time: 0.0,
30            current_count: 0,
31        }
32    }
33}
34
35impl ClickState {
36    fn register_click(&mut self, position: Point<Pixels>, time: f64) -> usize {
37        let distance = ((f32::from(position.x) - f32::from(self.last_position.x)).powi(2)
38            + (f32::from(position.y) - f32::from(self.last_position.y)).powi(2))
39        .sqrt();
40
41        if (time - self.last_time) < 400.0 && distance < 5.0 {
42            self.current_count += 1;
43        } else {
44            self.current_count = 1;
45        }
46
47        self.last_position = position;
48        self.last_time = time;
49        self.current_count
50    }
51}
52
53impl WebWindowInner {
54    pub fn register_event_listeners(self: &Rc<Self>) -> WebEventListeners {
55        let mut closures = vec![
56            self.register_pointer_down(),
57            self.register_pointer_up(),
58            self.register_pointer_move(),
59            self.register_pointer_leave(),
60            self.register_wheel(),
61            self.register_context_menu(),
62            self.register_dragover(),
63            self.register_drop(),
64            self.register_dragleave(),
65            self.register_key_down(),
66            self.register_key_up(),
67            self.register_paste(),
68            self.register_composition_start(),
69            self.register_composition_update(),
70            self.register_composition_end(),
71            self.register_focus(),
72            self.register_blur(),
73            self.register_pointer_enter(),
74            self.register_pointer_leave_hover(),
75        ];
76        closures.extend(self.register_visibility_change());
77        closures.extend(self.register_appearance_change());
78
79        WebEventListeners { closures }
80    }
81
82    fn listen(
83        self: &Rc<Self>,
84        event_name: &str,
85        handler: impl FnMut(JsValue) + 'static,
86    ) -> Closure<dyn FnMut(JsValue)> {
87        let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
88        self.canvas
89            .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
90            .ok();
91        closure
92    }
93
94    fn listen_input(
95        self: &Rc<Self>,
96        event_name: &str,
97        handler: impl FnMut(JsValue) + 'static,
98    ) -> Closure<dyn FnMut(JsValue)> {
99        let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
100        self.input_element
101            .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())
102            .ok();
103        closure
104    }
105
106    /// Registers a listener with `{passive: false}` so that `preventDefault()` works.
107    /// Needed for events like `wheel` which are passive by default in modern browsers.
108    fn listen_non_passive(
109        self: &Rc<Self>,
110        event_name: &str,
111        handler: impl FnMut(JsValue) + 'static,
112    ) -> Closure<dyn FnMut(JsValue)> {
113        let closure = Closure::<dyn FnMut(JsValue)>::new(handler);
114        let canvas_js: &JsValue = self.canvas.as_ref();
115        let callback_js: &JsValue = closure.as_ref();
116        let options = js_sys::Object::new();
117        js_sys::Reflect::set(&options, &"passive".into(), &false.into()).ok();
118        if let Ok(add_fn_val) = js_sys::Reflect::get(canvas_js, &"addEventListener".into()) {
119            if let Ok(add_fn) = add_fn_val.dyn_into::<js_sys::Function>() {
120                add_fn
121                    .call3(canvas_js, &event_name.into(), callback_js, &options)
122                    .ok();
123            }
124        }
125        closure
126    }
127
128    fn dispatch_input(&self, input: PlatformInput) -> Option<DispatchEventResult> {
129        let mut borrowed = self.callbacks.borrow_mut();
130        borrowed.input.as_mut().map(|callback| callback(input))
131    }
132
133    fn register_pointer_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
134        let this = Rc::clone(self);
135        self.listen("pointerdown", move |event: JsValue| {
136            let event: web_sys::PointerEvent = event.unchecked_into();
137            event.prevent_default();
138            this.input_element.focus().ok();
139
140            let button = dom_mouse_button_to_gpui(event.button());
141            let position = pointer_position_in_element(&event);
142            let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
143            let time = js_sys::Date::now();
144
145            this.pressed_button.set(Some(button));
146            let click_count = this.click_state.borrow_mut().register_click(position, time);
147
148            {
149                let mut current_state = this.state.borrow_mut();
150                current_state.mouse_position = position;
151                current_state.modifiers = modifiers;
152            }
153
154            this.dispatch_input(PlatformInput::MouseDown(MouseDownEvent {
155                button,
156                position,
157                modifiers,
158                click_count,
159                first_mouse: false,
160            }));
161        })
162    }
163
164    fn register_pointer_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
165        let this = Rc::clone(self);
166        self.listen("pointerup", move |event: JsValue| {
167            let event: web_sys::PointerEvent = event.unchecked_into();
168            event.prevent_default();
169
170            let button = dom_mouse_button_to_gpui(event.button());
171            let position = pointer_position_in_element(&event);
172            let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
173
174            this.pressed_button.set(None);
175            let click_count = this.click_state.borrow().current_count;
176
177            {
178                let mut current_state = this.state.borrow_mut();
179                current_state.mouse_position = position;
180                current_state.modifiers = modifiers;
181            }
182
183            this.dispatch_input(PlatformInput::MouseUp(MouseUpEvent {
184                button,
185                position,
186                modifiers,
187                click_count,
188            }));
189        })
190    }
191
192    fn register_pointer_move(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
193        let this = Rc::clone(self);
194        self.listen("pointermove", move |event: JsValue| {
195            let event: web_sys::PointerEvent = event.unchecked_into();
196            event.prevent_default();
197
198            let position = pointer_position_in_element(&event);
199            let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
200            let current_pressed = this.pressed_button.get();
201
202            {
203                let mut current_state = this.state.borrow_mut();
204                current_state.mouse_position = position;
205                current_state.modifiers = modifiers;
206            }
207
208            this.dispatch_input(PlatformInput::MouseMove(MouseMoveEvent {
209                position,
210                pressed_button: current_pressed,
211                modifiers,
212            }));
213        })
214    }
215
216    fn register_pointer_leave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
217        let this = Rc::clone(self);
218        self.listen("pointerleave", move |event: JsValue| {
219            let event: web_sys::PointerEvent = event.unchecked_into();
220
221            let position = pointer_position_in_element(&event);
222            let modifiers = modifiers_from_mouse_event(&event, this.is_mac);
223            let current_pressed = this.pressed_button.get();
224
225            {
226                let mut current_state = this.state.borrow_mut();
227                current_state.mouse_position = position;
228                current_state.modifiers = modifiers;
229            }
230
231            this.dispatch_input(PlatformInput::MouseExited(MouseExitEvent {
232                position,
233                pressed_button: current_pressed,
234                modifiers,
235            }));
236        })
237    }
238
239    fn register_wheel(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
240        let this = Rc::clone(self);
241        self.listen_non_passive("wheel", move |event: JsValue| {
242            let event: web_sys::WheelEvent = event.unchecked_into();
243            event.prevent_default();
244
245            let mouse_event: &web_sys::MouseEvent = event.as_ref();
246            let position = mouse_position_in_element(mouse_event);
247            let modifiers = modifiers_from_wheel_event(mouse_event, this.is_mac);
248
249            let delta_mode = event.delta_mode();
250            let delta = if delta_mode == 1 {
251                ScrollDelta::Lines(point(-event.delta_x() as f32, -event.delta_y() as f32))
252            } else {
253                ScrollDelta::Pixels(point(
254                    px(-event.delta_x() as f32),
255                    px(-event.delta_y() as f32),
256                ))
257            };
258
259            {
260                let mut current_state = this.state.borrow_mut();
261                current_state.modifiers = modifiers;
262            }
263
264            this.dispatch_input(PlatformInput::ScrollWheel(ScrollWheelEvent {
265                position,
266                delta,
267                modifiers,
268                touch_phase: TouchPhase::Moved,
269            }));
270        })
271    }
272
273    fn register_context_menu(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
274        self.listen("contextmenu", move |event: JsValue| {
275            let event: web_sys::Event = event.unchecked_into();
276            event.prevent_default();
277        })
278    }
279
280    fn register_dragover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
281        let this = Rc::clone(self);
282        self.listen("dragover", move |event: JsValue| {
283            let event: web_sys::DragEvent = event.unchecked_into();
284            event.prevent_default();
285
286            let mouse_event: &web_sys::MouseEvent = event.as_ref();
287            let position = mouse_position_in_element(mouse_event);
288
289            {
290                let mut current_state = this.state.borrow_mut();
291                current_state.mouse_position = position;
292            }
293
294            this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Pending { position }));
295        })
296    }
297
298    fn register_drop(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
299        let this = Rc::clone(self);
300        self.listen("drop", move |event: JsValue| {
301            let event: web_sys::DragEvent = event.unchecked_into();
302            event.prevent_default();
303
304            let mouse_event: &web_sys::MouseEvent = event.as_ref();
305            let position = mouse_position_in_element(mouse_event);
306
307            {
308                let mut current_state = this.state.borrow_mut();
309                current_state.mouse_position = position;
310            }
311
312            let paths = extract_file_paths_from_drag(&event);
313
314            this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Entered {
315                position,
316                paths: ExternalPaths(paths),
317            }));
318
319            this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Submit { position }));
320        })
321    }
322
323    fn register_dragleave(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
324        let this = Rc::clone(self);
325        self.listen("dragleave", move |_event: JsValue| {
326            this.dispatch_input(PlatformInput::FileDrop(FileDropEvent::Exited));
327        })
328    }
329
330    fn register_key_down(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
331        let this = Rc::clone(self);
332        self.listen_input("keydown", move |event: JsValue| {
333            let event: web_sys::KeyboardEvent = event.unchecked_into();
334
335            let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
336            let capslock = capslock_from_keyboard_event(&event);
337
338            {
339                let mut current_state = this.state.borrow_mut();
340                current_state.modifiers = modifiers;
341                current_state.capslock = capslock;
342            }
343
344            this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
345                modifiers,
346                capslock,
347            }));
348
349            let key = dom_key_to_gpui_key(&event);
350
351            if is_modifier_only_key(&key) {
352                return;
353            }
354
355            let is_held = event.repeat();
356            let key_char = compute_key_char(&event, &key, &modifiers);
357
358            let keystroke = Keystroke {
359                modifiers,
360                key,
361                key_char: key_char.clone(),
362            };
363
364            let result = this.dispatch_input(PlatformInput::KeyDown(KeyDownEvent {
365                keystroke,
366                is_held,
367                prefer_character_input: false,
368            }));
369
370            if let Some(result) = result {
371                if !result.propagate {
372                    event.prevent_default();
373                    return;
374                }
375            }
376
377            if this.is_composing.get() || event.is_composing() {
378                event.prevent_default();
379                return;
380            }
381
382            if modifiers.is_subset_of(&Modifiers::shift()) {
383                if let Some(text) = key_char {
384                    this.with_input_handler(|handler| {
385                        handler.replace_text_in_range(None, &text);
386                    });
387                    // The character went into the input handler; suppress browser
388                    // side-effects for the same keystroke (space scrolling the
389                    // page, quick-find, etc.). Everything not handled above falls
390                    // through so browser shortcuts keep their defaults.
391                    event.prevent_default();
392                }
393            }
394        })
395    }
396
397    fn register_key_up(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
398        let this = Rc::clone(self);
399        self.listen_input("keyup", move |event: JsValue| {
400            let event: web_sys::KeyboardEvent = event.unchecked_into();
401
402            let modifiers = modifiers_from_keyboard_event(&event, this.is_mac);
403            let capslock = capslock_from_keyboard_event(&event);
404
405            {
406                let mut current_state = this.state.borrow_mut();
407                current_state.modifiers = modifiers;
408                current_state.capslock = capslock;
409            }
410
411            this.dispatch_input(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
412                modifiers,
413                capslock,
414            }));
415
416            let key = dom_key_to_gpui_key(&event);
417
418            if is_modifier_only_key(&key) {
419                return;
420            }
421
422            let key_char = compute_key_char(&event, &key, &modifiers);
423
424            let keystroke = Keystroke {
425                modifiers,
426                key,
427                key_char,
428            };
429
430            let result = this.dispatch_input(PlatformInput::KeyUp(KeyUpEvent { keystroke }));
431            if let Some(result) = result {
432                if !result.propagate {
433                    event.prevent_default();
434                }
435            }
436        })
437    }
438
439    /// Paste is delivered through the DOM `paste` event rather than
440    /// `Platform::read_from_clipboard`: the browser's asynchronous clipboard
441    /// read API cannot fit that synchronous signature, while `ClipboardEvent`
442    /// exposes `clipboardData` synchronously inside the event. It fires for
443    /// any browser-initiated paste (keyboard, menu bar, context menu).
444    fn register_paste(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
445        let this = Rc::clone(self);
446        self.listen_input("paste", move |event: JsValue| {
447            let event: web_sys::ClipboardEvent = event.unchecked_into();
448            let Some(clipboard_data) = event.clipboard_data() else {
449                return;
450            };
451            let Ok(text) = clipboard_data.get_data("text/plain") else {
452                return;
453            };
454            if text.is_empty() {
455                return;
456            }
457
458            event.prevent_default();
459            this.with_input_handler(|handler| {
460                handler.replace_text_in_range(None, &text);
461            });
462        })
463    }
464
465    fn register_composition_start(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
466        let this = Rc::clone(self);
467        self.listen_input("compositionstart", move |_event: JsValue| {
468            this.is_composing.set(true);
469        })
470    }
471
472    fn register_composition_update(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
473        let this = Rc::clone(self);
474        self.listen_input("compositionupdate", move |event: JsValue| {
475            let event: web_sys::CompositionEvent = event.unchecked_into();
476            let data = event.data().unwrap_or_default();
477            this.is_composing.set(true);
478            this.with_input_handler(|handler| {
479                handler.replace_and_mark_text_in_range(None, &data, None);
480            });
481        })
482    }
483
484    fn register_composition_end(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
485        let this = Rc::clone(self);
486        self.listen_input("compositionend", move |event: JsValue| {
487            let event: web_sys::CompositionEvent = event.unchecked_into();
488            let data = event.data().unwrap_or_default();
489            this.is_composing.set(false);
490            this.with_input_handler(|handler| {
491                handler.replace_text_in_range(None, &data);
492                handler.unmark_text();
493            });
494            this.input_element.set_value("");
495        })
496    }
497
498    fn register_focus(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
499        let this = Rc::clone(self);
500        self.listen_input("focus", move |_event: JsValue| {
501            {
502                let mut state = this.state.borrow_mut();
503                state.is_active = true;
504            }
505            let mut callbacks = this.callbacks.borrow_mut();
506            if let Some(ref mut callback) = callbacks.active_status_change {
507                callback(true);
508            }
509        })
510    }
511
512    fn register_blur(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
513        let this = Rc::clone(self);
514        self.listen_input("blur", move |_event: JsValue| {
515            {
516                let mut state = this.state.borrow_mut();
517                state.is_active = false;
518            }
519            let mut callbacks = this.callbacks.borrow_mut();
520            if let Some(ref mut callback) = callbacks.active_status_change {
521                callback(false);
522            }
523        })
524    }
525
526    fn register_pointer_enter(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
527        let this = Rc::clone(self);
528        self.listen("pointerenter", move |_event: JsValue| {
529            {
530                let mut state = this.state.borrow_mut();
531                state.is_hovered = true;
532            }
533            let mut callbacks = this.callbacks.borrow_mut();
534            if let Some(ref mut callback) = callbacks.hover_status_change {
535                callback(true);
536            }
537        })
538    }
539
540    fn register_pointer_leave_hover(self: &Rc<Self>) -> Closure<dyn FnMut(JsValue)> {
541        let this = Rc::clone(self);
542        self.listen("pointerleave", move |_event: JsValue| {
543            {
544                let mut state = this.state.borrow_mut();
545                state.is_hovered = false;
546            }
547            let mut callbacks = this.callbacks.borrow_mut();
548            if let Some(ref mut callback) = callbacks.hover_status_change {
549                callback(false);
550            }
551        })
552    }
553}
554
555fn dom_key_to_gpui_key(event: &web_sys::KeyboardEvent) -> String {
556    let key = event.key();
557    match key.as_str() {
558        "Enter" => "enter".to_string(),
559        "Backspace" => "backspace".to_string(),
560        "Tab" => "tab".to_string(),
561        "Escape" => "escape".to_string(),
562        "Delete" => "delete".to_string(),
563        " " => "space".to_string(),
564        "ArrowLeft" => "left".to_string(),
565        "ArrowRight" => "right".to_string(),
566        "ArrowUp" => "up".to_string(),
567        "ArrowDown" => "down".to_string(),
568        "Home" => "home".to_string(),
569        "End" => "end".to_string(),
570        "PageUp" => "pageup".to_string(),
571        "PageDown" => "pagedown".to_string(),
572        "Insert" => "insert".to_string(),
573        "Control" => "control".to_string(),
574        "Alt" => "alt".to_string(),
575        "Shift" => "shift".to_string(),
576        "Meta" => "platform".to_string(),
577        "CapsLock" => "capslock".to_string(),
578        other => {
579            if let Some(rest) = other.strip_prefix('F') {
580                if let Ok(number) = rest.parse::<u8>() {
581                    if (1..=35).contains(&number) {
582                        return format!("f{number}");
583                    }
584                }
585            }
586            other.to_lowercase()
587        }
588    }
589}
590
591fn dom_mouse_button_to_gpui(button: i16) -> MouseButton {
592    match button {
593        0 => MouseButton::Left,
594        1 => MouseButton::Middle,
595        2 => MouseButton::Right,
596        3 => MouseButton::Navigate(NavigationDirection::Back),
597        4 => MouseButton::Navigate(NavigationDirection::Forward),
598        _ => MouseButton::Left,
599    }
600}
601
602fn modifiers_from_keyboard_event(event: &web_sys::KeyboardEvent, _is_mac: bool) -> Modifiers {
603    Modifiers {
604        control: event.ctrl_key(),
605        alt: event.alt_key(),
606        shift: event.shift_key(),
607        platform: event.meta_key(),
608        function: false,
609    }
610}
611
612fn modifiers_from_mouse_event(event: &web_sys::PointerEvent, _is_mac: bool) -> Modifiers {
613    let mouse_event: &web_sys::MouseEvent = event.as_ref();
614    Modifiers {
615        control: mouse_event.ctrl_key(),
616        alt: mouse_event.alt_key(),
617        shift: mouse_event.shift_key(),
618        platform: mouse_event.meta_key(),
619        function: false,
620    }
621}
622
623fn modifiers_from_wheel_event(event: &web_sys::MouseEvent, _is_mac: bool) -> Modifiers {
624    Modifiers {
625        control: event.ctrl_key(),
626        alt: event.alt_key(),
627        shift: event.shift_key(),
628        platform: event.meta_key(),
629        function: false,
630    }
631}
632
633fn capslock_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Capslock {
634    Capslock {
635        on: event.get_modifier_state("CapsLock"),
636    }
637}
638
639pub(crate) fn is_mac_platform(browser_window: &web_sys::Window) -> bool {
640    let navigator = browser_window.navigator();
641
642    #[allow(deprecated)]
643    // navigator.platform() is deprecated but navigator.userAgentData is not widely available yet
644    if let Ok(platform) = navigator.platform() {
645        if platform.contains("Mac") {
646            return true;
647        }
648    }
649
650    if let Ok(user_agent) = navigator.user_agent() {
651        return user_agent.contains("Mac");
652    }
653
654    false
655}
656
657fn is_modifier_only_key(key: &str) -> bool {
658    matches!(
659        key,
660        "control" | "alt" | "shift" | "platform" | "capslock" | "compose" | "process"
661    )
662}
663
664fn compute_key_char(
665    event: &web_sys::KeyboardEvent,
666    gpui_key: &str,
667    modifiers: &Modifiers,
668) -> Option<String> {
669    if modifiers.platform || modifiers.control {
670        return None;
671    }
672
673    if is_modifier_only_key(gpui_key) {
674        return None;
675    }
676
677    if gpui_key == "space" {
678        return Some(" ".to_string());
679    }
680
681    let raw_key = event.key();
682
683    if raw_key.len() == 1 {
684        return Some(raw_key);
685    }
686
687    None
688}
689
690fn pointer_position_in_element(event: &web_sys::PointerEvent) -> Point<Pixels> {
691    let mouse_event: &web_sys::MouseEvent = event.as_ref();
692    mouse_position_in_element(mouse_event)
693}
694
695fn mouse_position_in_element(event: &web_sys::MouseEvent) -> Point<Pixels> {
696    // offset_x/offset_y give position relative to the target element's padding edge
697    point(px(event.offset_x() as f32), px(event.offset_y() as f32))
698}
699
700fn extract_file_paths_from_drag(
701    event: &web_sys::DragEvent,
702) -> smallvec::SmallVec<[std::path::PathBuf; 2]> {
703    let mut paths = smallvec![];
704    let Some(data_transfer) = event.data_transfer() else {
705        return paths;
706    };
707    let file_list = data_transfer.files();
708    let Some(files) = file_list else {
709        return paths;
710    };
711    for index in 0..files.length() {
712        if let Some(file) = files.get(index) {
713            paths.push(std::path::PathBuf::from(file.name()));
714        }
715    }
716    paths
717}
718
Served at tenant.openagents/omega Member data and write actions are omitted.