Repository navigation
feat(onboarding): rework desktop onboarding and welcome timing #915
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
200259d
84d646f
13c3bac
bf72dfc
3afb260
6f4b5e7
37cc35c
599a3db
d3ec9df
0bdb268
765de18
9a376a8
b2e5f79
60d8ce3
d5f19f0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| //! Thin helpers around `Input.dispatchMouseEvent` and | ||
| //! `Input.dispatchKeyEvent` so providers can drive web UIs without | ||
| //! touching the page's JavaScript. | ||
| //! | ||
| //! All coordinates are CSS pixels relative to the viewport — the same | ||
| //! frame `DOMSnapshot.captureSnapshot(includeDOMRects=true)` returns | ||
| //! bounding rects in. Callers typically pair these with | ||
| //! [`crate::cdp::Snapshot::rect`] to find the click target. | ||
| //! | ||
| //! Everything here is CEF-only — CDP requires a remote-debugging port, | ||
| //! which wry doesn't expose. | ||
| //! | ||
| //! # Cookbook | ||
| //! | ||
| //! ```ignore | ||
| //! let snap = Snapshot::capture_with_rects(&mut cdp, &session).await?; | ||
| //! let idx = snap.find_descendant(0, |s, i| s.attr(i, "aria-label") == Some("Search mail")) | ||
| //! .ok_or("search box not found")?; | ||
| //! let rect = snap.rect(idx).ok_or("search box has no layout rect")?; | ||
| //! let (cx, cy) = rect.center(); | ||
| //! input::click(&mut cdp, &session, cx, cy).await?; | ||
| //! input::type_text(&mut cdp, &session, "from:linkedin.com").await?; | ||
| //! input::press_key(&mut cdp, &session, Key::Enter).await?; | ||
| //! ``` | ||
|
|
||
| use serde_json::{json, Value}; | ||
|
|
||
| use super::CdpConn; | ||
|
|
||
| #[allow(dead_code)] // helper is used by currently gated input paths. | ||
| #[cfg(target_os = "macos")] | ||
| const SELECT_ALL_MODIFIER: u32 = 4; | ||
| #[allow(dead_code)] // helper is used by currently gated input paths. | ||
| #[cfg(not(target_os = "macos"))] | ||
| const SELECT_ALL_MODIFIER: u32 = 2; | ||
|
|
||
| /// Names recognised by `Input.dispatchKeyEvent`'s `key` field. We | ||
| /// hand-pick the ones Gmail's keyboard handlers care about so callers | ||
| /// can use a typed value rather than stringly-typed literals scattered | ||
| /// across providers. | ||
| #[allow(dead_code)] // variants reserved for upcoming providers / write ops. | ||
| #[derive(Debug, Clone, Copy)] | ||
| pub enum Key { | ||
| Enter, | ||
| Escape, | ||
| Tab, | ||
| Backspace, | ||
| ArrowDown, | ||
| ArrowUp, | ||
| } | ||
|
|
||
| impl Key { | ||
| /// `(key, code, windowsVirtualKeyCode)` triple. Gmail's listeners | ||
| /// branch on different fields depending on browser; we set all three | ||
| /// to maximise compatibility. | ||
| fn cdp_fields(self) -> (&'static str, &'static str, u32) { | ||
| match self { | ||
| Key::Enter => ("Enter", "Enter", 13), | ||
| Key::Escape => ("Escape", "Escape", 27), | ||
| Key::Tab => ("Tab", "Tab", 9), | ||
| Key::Backspace => ("Backspace", "Backspace", 8), | ||
| Key::ArrowDown => ("ArrowDown", "ArrowDown", 40), | ||
| Key::ArrowUp => ("ArrowUp", "ArrowUp", 38), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Click at `(x, y)` — left button, no modifiers, single click. | ||
| /// Issues mouseMoved → mousePressed → mouseReleased so hover handlers | ||
| /// (Gmail's search-box has one) fire correctly before the click. | ||
| pub async fn click(cdp: &mut CdpConn, session: &str, x: f64, y: f64) -> Result<(), String> { | ||
| log::debug!("[cdp::input] click session={session} x={x:.1} y={y:.1}"); | ||
| let _ = mouse_event(cdp, session, "mouseMoved", x, y, 0).await?; | ||
| let _ = mouse_event(cdp, session, "mousePressed", x, y, 1).await?; | ||
| let _ = mouse_event(cdp, session, "mouseReleased", x, y, 1).await?; | ||
| log::debug!("[cdp::input] click complete session={session}"); | ||
| Ok(()) | ||
| } | ||
|
|
||
| async fn mouse_event( | ||
| cdp: &mut CdpConn, | ||
| session: &str, | ||
| kind: &str, | ||
| x: f64, | ||
| y: f64, | ||
| click_count: u32, | ||
| ) -> Result<Value, String> { | ||
| log::debug!( | ||
| "[cdp::input] mouse_event session={session} kind={kind} x={x:.1} y={y:.1} clicks={click_count}" | ||
| ); | ||
| cdp.call( | ||
| "Input.dispatchMouseEvent", | ||
| json!({ | ||
| "type": kind, | ||
| "x": x, | ||
| "y": y, | ||
| "button": "left", | ||
| "buttons": if kind == "mousePressed" { 1 } else { 0 }, | ||
| "clickCount": click_count, | ||
| }), | ||
| Some(session), | ||
| ) | ||
| .await | ||
| .map_err(|e| format!("Input.dispatchMouseEvent {kind}: {e}")) | ||
| } | ||
|
|
||
| /// Type a literal string by dispatching one `keyDown`/`char`/`keyUp` | ||
| /// triple per character. CDP's `dispatchKeyEvent type=char` is what | ||
| /// actually inserts text into focused editable fields — `keyDown` | ||
| /// alone leaves the input empty for most letters. The `keyDown` | ||
| /// + `keyUp` pair is still needed so listeners (autocomplete, | ||
| /// keystroke counters) see a normal keystroke. | ||
| pub async fn type_text(cdp: &mut CdpConn, session: &str, text: &str) -> Result<(), String> { | ||
| log::debug!( | ||
| "[cdp::input] type_text session={session} chars={}", | ||
| text.chars().count() | ||
| ); | ||
| for ch in text.chars() { | ||
| let s = ch.to_string(); | ||
| // keyDown — Gmail's command/keyboard router observes these. | ||
| cdp.call( | ||
| "Input.dispatchKeyEvent", | ||
| json!({ | ||
| "type": "keyDown", | ||
| "text": s, | ||
| "unmodifiedText": s, | ||
| "key": s, | ||
| }), | ||
| Some(session), | ||
| ) | ||
| .await | ||
| .map_err(|e| format!("Input.dispatchKeyEvent keyDown {ch:?}: {e}"))?; | ||
| // char — actual text insertion into the focused editable. | ||
| cdp.call( | ||
| "Input.dispatchKeyEvent", | ||
| json!({ | ||
| "type": "char", | ||
| "text": s, | ||
| "unmodifiedText": s, | ||
| "key": s, | ||
| }), | ||
| Some(session), | ||
| ) | ||
| .await | ||
| .map_err(|e| format!("Input.dispatchKeyEvent char {ch:?}: {e}"))?; | ||
| cdp.call( | ||
| "Input.dispatchKeyEvent", | ||
| json!({ | ||
| "type": "keyUp", | ||
| "text": s, | ||
| "unmodifiedText": s, | ||
| "key": s, | ||
| }), | ||
| Some(session), | ||
| ) | ||
| .await | ||
| .map_err(|e| format!("Input.dispatchKeyEvent keyUp {ch:?}: {e}"))?; | ||
| } | ||
| log::debug!("[cdp::input] type_text complete session={session}"); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Press a non-character key (Enter, Esc, …). Sends `rawKeyDown` → | ||
| /// `keyUp`; no `char` because non-printables don't insert text. | ||
| pub async fn press_key(cdp: &mut CdpConn, session: &str, key: Key) -> Result<(), String> { | ||
| let (key_name, code, vk) = key.cdp_fields(); | ||
| log::debug!("[cdp::input] press_key session={session} key={key_name}"); | ||
| cdp.call( | ||
| "Input.dispatchKeyEvent", | ||
| json!({ | ||
| "type": "rawKeyDown", | ||
| "key": key_name, | ||
| "code": code, | ||
| "windowsVirtualKeyCode": vk, | ||
| "nativeVirtualKeyCode": vk, | ||
| }), | ||
| Some(session), | ||
| ) | ||
| .await | ||
| .map_err(|e| format!("Input.dispatchKeyEvent rawKeyDown {key_name}: {e}"))?; | ||
| cdp.call( | ||
| "Input.dispatchKeyEvent", | ||
| json!({ | ||
| "type": "keyUp", | ||
| "key": key_name, | ||
| "code": code, | ||
| "windowsVirtualKeyCode": vk, | ||
| "nativeVirtualKeyCode": vk, | ||
| }), | ||
| Some(session), | ||
| ) | ||
| .await | ||
| .map_err(|e| format!("Input.dispatchKeyEvent keyUp {key_name}: {e}"))?; | ||
| log::debug!("[cdp::input] press_key complete session={session} key={key_name}"); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Dispatch Cmd/Ctrl+A to select-all in the focused contenteditable / input. | ||
| /// Useful when the search box already has a previous query in it that | ||
| /// we need to overwrite — Gmail keeps the last query rendered in the | ||
| /// search input so a fresh visit sees stale text. | ||
| pub async fn select_all_in_focused(cdp: &mut CdpConn, session: &str) -> Result<(), String> { | ||
| log::debug!( | ||
| "[cdp::input] select_all_in_focused session={session} modifier={SELECT_ALL_MODIFIER}" | ||
| ); | ||
| cdp.call( | ||
| "Input.dispatchKeyEvent", | ||
| json!({ | ||
| "type": "rawKeyDown", | ||
| "key": "a", | ||
| "code": "KeyA", | ||
| "windowsVirtualKeyCode": 65, | ||
| "nativeVirtualKeyCode": 65, | ||
| "modifiers": SELECT_ALL_MODIFIER, | ||
| }), | ||
| Some(session), | ||
| ) | ||
| .await | ||
| .map_err(|e| format!("Input.dispatchKeyEvent select-all keyDown: {e}"))?; | ||
| cdp.call( | ||
| "Input.dispatchKeyEvent", | ||
| json!({ | ||
| "type": "keyUp", | ||
| "key": "a", | ||
| "code": "KeyA", | ||
| "windowsVirtualKeyCode": 65, | ||
| "nativeVirtualKeyCode": 65, | ||
| "modifiers": SELECT_ALL_MODIFIER, | ||
| }), | ||
| Some(session), | ||
| ) | ||
| .await | ||
| .map_err(|e| format!("Input.dispatchKeyEvent select-all keyUp: {e}"))?; | ||
| log::debug!("[cdp::input] select_all_in_focused complete session={session}"); | ||
| Ok(()) | ||
| } | ||
|
Comment on lines
+71
to
+236
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add unit tests for new CDP input primitives before merge. This introduces core input behavior but does not include matching Rust unit coverage in the changes shown. Please add tests that assert event order and payload fields for As per coding guidelines, "Ship unit tests and coverage for behavior you are adding or changing before building additional features on top." 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |
|
|
||
| pub mod conn; | ||
| pub mod emulation; | ||
| pub mod input; | ||
| pub mod session; | ||
| pub mod snapshot; | ||
| pub mod target; | ||
|
|
@@ -19,12 +20,16 @@ pub use emulation::{set_user_agent_override, UaSpec}; | |
| pub use session::{ | ||
| placeholder_marker, placeholder_url, spawn_session, target_url_fragment, SpawnedSession, | ||
| }; | ||
| pub use snapshot::Snapshot; | ||
| #[allow(unused_imports)] // `Rect` re-export consumed once turn 2 lands; keep stable. | ||
| pub use snapshot::{Rect, Snapshot}; | ||
| pub use target::{ | ||
| browser_ws_url, connect_and_attach_matching, detach_session, find_page_target_where, | ||
| }; | ||
|
|
||
| /// Remote debugging host — matches `--remote-debugging-port=9222` in | ||
| /// `lib.rs`. Kept as constants so scanners and the session opener agree. | ||
| /// Remote debugging host — matches `--remote-debugging-port=19222` in | ||
| /// `lib.rs`. Kept as constants so scanners and the session opener | ||
| /// agree. Port was 9222 originally but collided with ollama's | ||
| /// `127.0.0.1:9222` listener (silent CDP-attach failure → blank | ||
| /// child webviews). If you change either constant, update both. | ||
| pub const CDP_HOST: &str = "127.0.0.1"; | ||
| pub const CDP_PORT: u16 = 9222; | ||
| pub const CDP_PORT: u16 = 19222; | ||
|
Comment on lines
+29
to
+35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check that whatsapp_scanner uses CDP_PORT from the cdp module, not a hardcoded value
rg -n 'CDP_PORT|19222|9222' --type rust app/src-tauri/src/whatsapp_scanner/Repository: tinyhumansai/openhuman Length of output: 703 Consolidate CDP_PORT to avoid duplicate constant definitions. The Import 🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.