diff --git a/crates/shell/resources/glance/notice.card b/crates/shell/resources/glance/notice.card index 1526881d..9de6bb8b 100644 --- a/crates/shell/resources/glance/notice.card +++ b/crates/shell/resources/glance/notice.card @@ -13,11 +13,11 @@ source note sys.dataset(fields: [title, summary, as_of]) copy app { class: vocabulary, en: "{app}" } -view root Surface(pad: .tight) { - Col(gap: 8) { +view root Panel { + Col(gap: 10) { Row(gap: 8, align: .center) { Icon(name: .{icon}, size: .row) - TextCaption(text: copy.app, width: .fill) + TextCaption(text: copy.app) TextCaption(value: note.as_of) } TextTitle(text: note.title, width: .fill) diff --git a/crates/shell/src/desktop_app.rs b/crates/shell/src/desktop_app.rs index 5a1ce24e..99686dc4 100644 --- a/crates/shell/src/desktop_app.rs +++ b/crates/shell/src/desktop_app.rs @@ -42,6 +42,7 @@ impl App { /// sits on the wallpaper with no flat wash beneath; under flat it is /// `mod.wm_theme.background` again, as the DSL resolved it. pub(super) fn apply_material_to_chrome(&mut self, cx: &mut Cx, material: shell::MaterialTokens, palette: Option) { + let style = self.state_mut().style.target; if let Some(mut w) = self.ui.widget(cx, ids!(shell_bar)).borrow_mut::() { w.set_material(material, palette); } @@ -53,6 +54,8 @@ impl App { } if let Some(mut w) = self.ui.widget(cx, ids!(shell_notes)).borrow_mut::() { w.set_material(material, palette); + w.mobile = style.mobile(); + w.icon_style = style; } if let Some(mut w) = self.ui.widget(cx, ids!(shell_osd)).borrow_mut::() { w.set_material(material, palette); @@ -62,6 +65,7 @@ impl App { } if let Some(mut w) = self.ui.widget(cx, ids!(shell_glance_sheet)).borrow_mut::() { w.set_material(material, palette); + w.mobile = style.mobile(); } // The chat panes: the system chat and "Ask ". for pane in [ids!(shell_system_chat), ids!(shell_app_chat)] { diff --git a/crates/shell/src/glance_card.rs b/crates/shell/src/glance_card.rs index 82947f20..e184c1c9 100644 --- a/crates/shell/src/glance_card.rs +++ b/crates/shell/src/glance_card.rs @@ -146,7 +146,17 @@ fn lower_report(source: &str, report: octoscript_ui_l0::RealizeReport, l0_ui: bo } } let kit_source = [PALETTE_BASE, delta, DERIVE_COLOR, DERIVE, KIT, HOST_KIT, &octoscript_ui_l0::kit::lower(root)].join("\n"); - let tree = octoscript_makepad::design::prepare(&kit_source)?; + let mut tree = octoscript_makepad::design::prepare(&kit_source)?; + if root.kind == "Panel" { + // This panel IS the hosted card, not a panel inside a page. The + // host clips to its measured bounds and owns the outer spacing; + // page margins and an outward shadow otherwise leave a clipped + // rectangle behind rounded corners and duplicate the sheet's frame. + tree.attrs.margintop = None; + tree.attrs.marginx = None; + tree.attrs.elevation = None; + tree.attrs.border = None; + } // A measured design (an imported artboard) lowers as the Card runner // lowers it; a kit-composed card (columns, rows, text) through the // backend's general translation. diff --git a/crates/shell/src/glance_sheet.rs b/crates/shell/src/glance_sheet.rs index 8e526e5e..107d1805 100644 --- a/crates/shell/src/glance_sheet.rs +++ b/crates/shell/src/glance_sheet.rs @@ -31,6 +31,16 @@ const MARGIN: f64 = 16.0; const HEADER: f64 = 44.0; const PAD: f64 = 12.0; const CLOSE: f64 = 28.0; +const PHONE_HEADER: f64 = 52.0; + +fn phone_sheet_rect(screen: Rect, card_h: f64, safe: makepad_platform::event::SafeAreaInsets) -> Rect { + let available = screen.size.x - safe.left - safe.right; + let w = (available - 24.0).clamp(0.0, 420.0); + let h = (PHONE_HEADER + card_h + PAD).max(SHEET_MIN_HEIGHT) + .min((screen.size.y - safe.top - safe.bottom - 32.0).max(0.0)); + rect(screen.pos.x + safe.left + (available - w) * 0.5, + screen.pos.y + screen.size.y - safe.bottom - 16.0 - h, w, h) +} script_mod! { use mod.prelude.widgets_internal.* @@ -103,9 +113,20 @@ pub struct ShellGlanceSheet { #[redraw] #[rust] area: Area, + // Close on the matching release, retaining the whole gesture so its + // release cannot activate a card or control below the modal surface. + #[rust] + close_touch: Option<(u64, Vec2d, bool)>, + #[rust] + pub mobile: bool, } impl ShellGlanceSheet { + fn close_target(&self) -> Rect { + if self.mobile { + rect(self.sheet.pos.x + self.sheet.size.x - 52.0, self.sheet.pos.y + 4.0, 44.0, 44.0) + } else { close_rect(self.sheet) } + } /// The key (`app/card_id`) of the open card. pub fn open_key(&self) -> Option<&str> { self.open.as_ref().map(|o| o.key.as_str()) @@ -129,6 +150,7 @@ impl ShellGlanceSheet { } pub fn close(&mut self, cx: &mut Cx) { + self.close_touch = None; if let Some(open) = self.open.take() { log!("glance sheet: closed {}", open.key); } @@ -169,16 +191,31 @@ impl Widget for ShellGlanceSheet { self.d.begin_surface(cx); if let Some(open) = &self.open { let tok = self.d.tokens(self.tokens); - let ink = tok.notifications.surface.text; - self.d.solid(cx, screen, vec4(0.0, 0.0, 0.0, 0.55)); + let (fill, ink) = if self.mobile { + crate::shell::ui::phone_card_colors(crate::glance_card::dark()) + } else { (tok.notifications.surface.bg(), tok.notifications.surface.text) }; + self.d.solid(cx, screen, vec4(0.0, 0.0, 0.0, if self.mobile { 0.32 } else { 0.55 })); let card_h = crate::glance_card::measured_height(&Self::tile_key(&open.key)).unwrap_or(UNMEASURED_CARD); - let sheet = sheet_rect(screen, card_h); + let safe = cx.display_context.safe_area_insets; + let sheet = if self.mobile { phone_sheet_rect(screen, card_h, safe) } + else { sheet_rect(screen, card_h) }; self.sheet = sheet; - self.d.card(cx, sheet, &tok.notifications.surface); - let close = close_rect(sheet); - self.d.label_elided(cx, rect(sheet.pos.x + PAD + 4.0, sheet.pos.y + 4.0, sheet.size.x - PAD * 2.0 - CLOSE - 8.0, HEADER - 4.0), false, 12.0, alpha(ink, 0.7), HAlign::Left, &open.card.title); - self.d.icon_centered(cx, Ico::Close, close, 14.0, ink); - let card = card_rect(sheet); + let close = if self.mobile { + rect(sheet.pos.x + sheet.size.x - 52.0, sheet.pos.y + 4.0, 44.0, 44.0) + } else { close_rect(sheet) }; + let card = if self.mobile { + self.d.phone_card(cx, sheet, fill, 1.0); + self.d.label(cx, rect(sheet.pos.x + 20.0, sheet.pos.y + 8.0, sheet.size.x - 80.0, 36.0), + true, 12.0, alpha(ink, 0.65), HAlign::Left, "Notification"); + self.d.rounded(cx, rect(close.pos.x + 7.0, close.pos.y + 7.0, 30.0, 30.0), 15.0, alpha(ink, 0.06)); + rect(sheet.pos.x + PAD, sheet.pos.y + PHONE_HEADER, sheet.size.x - PAD * 2.0, + sheet.size.y - PHONE_HEADER - PAD) + } else { + self.d.card(cx, sheet, &tok.notifications.surface); + self.d.label_elided(cx, rect(sheet.pos.x + PAD + 4.0, sheet.pos.y + 4.0, sheet.size.x - PAD * 2.0 - CLOSE - 8.0, HEADER - 4.0), false, 12.0, alpha(ink, 0.7), HAlign::Left, &open.card.title); + card_rect(sheet) + }; + self.d.icon_centered(cx, Ico::Close, close, if self.mobile { 12.0 } else { 14.0 }, ink); let key = Self::tile_key(&open.key); let body = self.live.body(&key, &open.card, "glance sheet"); self.tiles.draw(cx, &key, &open.card.app, open.card.contained, &body, card); @@ -194,11 +231,40 @@ impl Widget for ShellGlanceSheet { } fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) { + if matches!(event, Event::Pause | Event::Background) { self.close_touch = None; } if self.open.is_none() { return; } + if let Event::TouchUpdate(e) = event { + use makepad_platform::event::TouchState; + if self.close_touch.as_ref().is_some_and(|(uid, _, _)| !e.touches.iter().any(|t| + t.uid == *uid && t.state != TouchState::Start)) { + self.close_touch = None; + } + let sheet = self.sheet; + let close = self.close_target(); + let closes = |p| contains(close, p) || !contains(sheet, p); + let mut consumed = self.close_touch.is_some(); + for t in &e.touches { + if self.close_touch.is_none() && t.state == TouchState::Start && closes(t.abs) { + self.close_touch = Some((t.uid, t.abs, true)); + consumed = true; + } + if let Some((uid, start, armed)) = self.close_touch.as_mut() { + if *uid == t.uid { + if (t.abs - *start).length() > 12.0 { *armed = false; } + if t.state == TouchState::Stop { + let should_close = *armed && closes(t.abs); + self.close_touch = None; + if should_close { self.close(cx); return; } + } + } + } + } + if consumed { return; } + } match event { - Event::MouseDown(e) if contains(close_rect(self.sheet), e.abs) || !contains(self.sheet, e.abs) => { + Event::MouseDown(e) if contains(self.close_target(), e.abs) || !contains(self.sheet, e.abs) => { self.close(cx); return; } @@ -217,6 +283,90 @@ impl Widget for ShellGlanceSheet { mod tests { use super::*; + #[test] + fn phone_sheet_and_close_target_stay_inside_system_insets() { + use makepad_platform::event::SafeAreaInsets; + let mut cx = Cx::new(Box::new(|_, _| {})); + for (w, h) in [(320.0, 640.0), (384.0, 810.0), (810.0, 384.0)] { + let safe = SafeAreaInsets { top: 32.0, right: 24.0, bottom: 24.0, left: 8.0 }; + let mut sheet = opened(&mut cx); + sheet.mobile = true; + sheet.sheet = phone_sheet_rect(rect(0.0, 0.0, w, h), 900.0, safe); + let r = sheet.sheet; + assert!(r.pos.x >= safe.left + 12.0 && r.pos.y >= safe.top + 16.0); + assert!(r.pos.x + r.size.x <= w - safe.right - 12.0); + assert!(r.pos.y + r.size.y <= h - safe.bottom - 16.0); + let close = sheet.close_target(); + assert_eq!(close.size, dvec2(44.0, 44.0)); + let at = close.pos + close.size * 0.5; + touch(&mut sheet, &mut cx, makepad_platform::event::TouchState::Start, at); + touch(&mut sheet, &mut cx, makepad_platform::event::TouchState::Stop, at); + assert!(!sheet.is_open()); + } + } + + fn opened(cx: &mut Cx) -> ShellGlanceSheet { + let mut sheet = cx.with_vm(ShellGlanceSheet::script_new); + sheet.sheet = rect(20., 60., 340., 400.); + sheet.open = Some(Open { key: "test/card".into(), card: GlanceCard { + app: "test".into(), card_id: "card".into(), title: "Card".into(), priority: 0, + published_ms: 0, expires_ms: u64::MAX, open_app: "test".into(), route: None, + body: "".into(), contained: false, digests: Vec::new(), l0: None, + }}); + sheet + } + fn touch(sheet: &mut ShellGlanceSheet, cx: &mut Cx, state: makepad_platform::event::TouchState, abs: Vec2d) { + use makepad_platform::event::{TouchPoint, TouchUpdateEvent}; + let event = Event::TouchUpdate(TouchUpdateEvent { + time: 0., window_id: CxWindowPool::id_zero(), modifiers: Default::default(), + touches: vec![TouchPoint { state, abs, time: 0., uid: 9, rotation_angle: 0., force: 0., + radius: dvec2(1., 1.), handled: Default::default(), sweep_lock: Default::default() }], + }); + sheet.handle_event(cx, &event, &mut Scope::empty()); + } + #[test] + fn phone_card_close_and_backdrop_taps_close_on_release_only() { + use makepad_platform::event::TouchState::*; + let mut cx = Cx::new(Box::new(|_, _| {})); + for at in [dvec2(330., 82.), dvec2(5., 500.)] { + let mut sheet = opened(&mut cx); + touch(&mut sheet, &mut cx, Start, at); + assert!(sheet.is_open(), "the modal keeps ownership through release"); + touch(&mut sheet, &mut cx, Stop, at); + assert!(!sheet.is_open()); + assert!(sheet.close_touch.is_none()); + } + } + #[test] + fn dragging_from_the_phone_card_close_target_does_not_close_it() { + use makepad_platform::event::TouchState::*; + let mut cx = Cx::new(Box::new(|_, _| {})); + let mut sheet = opened(&mut cx); + let at = dvec2(330., 82.); + touch(&mut sheet, &mut cx, Start, at); + touch(&mut sheet, &mut cx, Move, at + dvec2(0., 60.)); + touch(&mut sheet, &mut cx, Stop, at); + assert!(sheet.is_open()); + assert!(sheet.close_touch.is_none()); + } + + #[test] + fn a_modal_consuming_card_close_release_does_not_poison_the_next_gesture() { + use makepad_platform::event::TouchState::*; + let mut cx = Cx::new(Box::new(|_, _| {})); + let mut sheet = opened(&mut cx); + touch(&mut sheet, &mut cx, Start, dvec2(330., 82.)); + // Stop is consumed above this sheet. A new Start reusing Android's + // pointer ID inside the card must cancel that old close gesture. + touch(&mut sheet, &mut cx, Start, dvec2(100., 200.)); + assert!(sheet.close_touch.is_none()); + touch(&mut sheet, &mut cx, Stop, dvec2(100., 200.)); + assert!(sheet.is_open()); + touch(&mut sheet, &mut cx, Start, dvec2(330., 82.)); + touch(&mut sheet, &mut cx, Stop, dvec2(330., 82.)); + assert!(!sheet.is_open()); + } + #[test] fn the_window_is_centred_and_sized_to_its_card() { let screen = rect(0.0, 0.0, 1280.0, 800.0); diff --git a/crates/shell/src/lib.rs b/crates/shell/src/lib.rs index 1ae2a82b..99f6c85c 100644 --- a/crates/shell/src/lib.rs +++ b/crates/shell/src/lib.rs @@ -226,11 +226,6 @@ script_mod! { shell_glance := ShellGlancePanel{} shell_panel := ShellPanel{} shell_menu := ShellMenu{} - // One glance card, full size, over a dimmed desk - // (glance_sheet.rs): a card's toast opens it. - shell_glance_sheet := ShellGlanceSheet{} - shell_notes := ShellNotifications{} - shell_osd := ShellOsd{} // The approval surface (approvals/): the shell's // approval and first-use sheets, the time-box // indicator, and Settings > Assistant > Approvals. @@ -241,6 +236,11 @@ script_mod! { // "Ask " (app_chat/): an app agent's // conversation, both lanes, beside the system chat. shell_app_chat := ShellSystemChat{ app_panel: true } + // A card and its notification remain above full-screen + // phone chat; approvals and the developer banner win. + shell_glance_sheet := ShellGlanceSheet{} + shell_notes := ShellNotifications{} + shell_osd := ShellOsd{} shell_approvals := ShellApprovals{} shell_approvals_settings := ShellApprovalsSettings{} // Developer mode's banner (dev_mode.rs): over @@ -3794,22 +3794,45 @@ impl App { return false; } sheet.handle_event(cx, event, &mut Scope::empty()); - if matches!(event, Event::MouseDown(_) | Event::MouseUp(_)) { + if matches!(event, Event::MouseDown(_) | Event::MouseUp(_) | Event::TouchUpdate(_)) { self.redraw_all(cx); } true } + /// Close the top card before Back can reach a chat or the app beneath it. + fn close_glance_card(&mut self, cx: &mut Cx) -> bool { + let sheet = self.ui.widget(cx, ids!(shell_glance_sheet)); + let closed = sheet.borrow_mut::().is_some_and(|mut s| { + if !s.is_open() { return false; } + s.close(cx); + true + }); + if closed { self.redraw_all(cx); } + closed + } + /// Open the published card `key` in the card window; when it is gone /// (withdrawn, expired), the glance panel instead. fn open_glance_card(&mut self, cx: &mut Cx, key: &str) { self.set_glance_open(cx, false); + let phone = self.state.as_ref().is_some_and(|s| s.style.target.mobile()); + if phone { + // Closing the card returns to its live glance feed. Chat turns + // continue; only their panes are hidden, as with their Close button. + self.close_chat_panes(cx, true); + let state = self.state_mut(); + state.phone.shade.close(); + state.phone.navigate(mobile::PhoneScreen::Home); + state.phone.pages.jump(-1); + self.animate_phone(cx); + } let opened = self.ui.widget(cx, ids!(shell_glance_sheet)).borrow_mut::().is_some_and(|mut s| s.open_card(cx, key)); if opened { log!("wm: glance toast opens card {key}"); } else { - log!("wm: glance card {key} is gone; opening the glance panel"); - self.set_glance_open(cx, true); + log!("wm: glance card {key} is gone; opening the glance feed"); + if !phone { self.set_glance_open(cx, true); } } self.redraw_all(cx); } @@ -4072,14 +4095,11 @@ impl App { } } - /// A press on a toast (shell/notifications.rs `hit`). Only the press: a - /// drag of a pane's frame keeps its moves and its release wherever they - /// go. + /// A toast owns a touch from its press through release, including a + /// cancelled drag. None of that gesture can leak into the pane beneath it. fn press_on_toast(&mut self, cx: &mut Cx, event: &Event) -> bool { - let Event::MouseDown(e) = event else { return false }; - let notes = self.ui.widget(cx, ids!(shell_notes)); - let on = notes.borrow::().is_some_and(|n| n.hit(e.abs)); - on + self.ui.widget(cx, ids!(shell_notes)).borrow_mut::() + .is_some_and(|mut n| n.owns_pointer(event)) } /// The system chat's pane owns the pointer inside its rect while open. @@ -6365,7 +6385,8 @@ impl App { // is not also broadcast through the widget tree. if event.back_pressed() && self.state.as_ref().is_some_and(|state| state.style.target.mobile()) { log!("[phone] back"); - // An open assistant pane takes Back first, as its own Close does. + if self.close_glance_card(cx) { return; } + // An open assistant pane takes Back next, as its own Close does. if self.close_chat_panes(cx, false) { return; } self.phone_action(cx, mobile::PhoneHit::Back); return; diff --git a/crates/shell/src/mobile_app.rs b/crates/shell/src/mobile_app.rs index bb51a3e5..7ebfbcfb 100644 --- a/crates/shell/src/mobile_app.rs +++ b/crates/shell/src/mobile_app.rs @@ -579,6 +579,7 @@ impl App { PhoneHit::Symbols=>{let p=&mut self.state_mut().phone;p.symbols=!p.symbols;} PhoneHit::Key(key)=>self.type_phone_key(cx,&key), PhoneHit::Back=>{ + if self.close_glance_card(cx) {return;} if self.state_mut().phone.keyboard_target>0.0 {self.dismiss_phone_keyboard(cx);} else {self.phone_back(cx);} } diff --git a/crates/shell/src/shell/notifications.rs b/crates/shell/src/shell/notifications.rs index 52e3ea68..df3a7333 100644 --- a/crates/shell/src/shell/notifications.rs +++ b/crates/shell/src/shell/notifications.rs @@ -184,6 +184,11 @@ pub enum ShellNotificationsAction { None, } +#[derive(Clone, Copy, Debug, PartialEq)] +enum ToastTarget { More, Activate(u64), Dismiss(u64), Action(u64) } +struct ToastTouch { uid: u64, start: Vec2d, target: Option } +const TOUCH_SLOP: f64 = 12.0; + #[derive(Script, ScriptHook, Widget)] pub struct ShellNotifications { #[uid] @@ -212,6 +217,8 @@ pub struct ShellNotifications { #[rust] pub icon_style: DesktopStyle, #[rust] + pub mobile: bool, + #[rust] app_icons: AppIconDraw, #[rust] area: Area, @@ -237,6 +244,8 @@ pub struct ShellNotifications { /// What the last layout log said, so it is logged once per change. #[rust] logged: String, + #[rust] + touch: Option, } impl ShellNotifications { @@ -304,6 +313,75 @@ impl ShellNotifications { !self.inert && (contains(self.more, p) || self.card_rects.iter().any(|(_, card, close)| contains(*card, p) || contains(*close, p))) } + fn target(&self, p: Vec2d) -> Option { + if self.inert { return None; } + if contains(self.more, p) { return Some(ToastTarget::More); } + let live = |id| self.live.iter().any(|entry| entry.note.id == id); + if let Some((id, _)) = self.action_rects.iter().find(|(id, r)| live(*id) && contains(*r, p)) { + return Some(ToastTarget::Action(*id)); + } + self.card_rects.iter().find_map(|(id, card, close)| { + if !live(*id) { None } + else if contains(*close, p) { Some(ToastTarget::Dismiss(*id)) } + else if contains(*card, p) { Some(ToastTarget::Activate(*id)) } + else { None } + }) + } + + fn reconcile_touch(&mut self, event: &makepad_platform::event::TouchUpdateEvent) { + use makepad_platform::event::TouchState; + // A higher modal can consume Stop. TouchUpdate lists current fingers; + // an absent ID (or a recycled ID's fresh Start) ends the old capture. + if self.touch.as_ref().is_some_and(|held| !event.touches.iter().any(|t| + t.uid == held.uid && t.state != TouchState::Start)) { + self.touch = None; + } + } + + pub fn owns_pointer(&mut self, event: &Event) -> bool { + if self.inert { self.touch = None; return false; } + match event { + Event::MouseDown(e) => self.hit(e.abs), + Event::TouchUpdate(e) => { + self.reconcile_touch(e); + self.touch.is_some() || e.touches.iter().any(|t| + t.state == makepad_platform::event::TouchState::Start && self.target(t.abs).is_some()) + } + _ => false, + } + } + + fn activate_target(&mut self, cx: &mut Cx, target: ToastTarget) { + let (id, action) = match target { + ToastTarget::More => { self.expanded = true; self.redraw(cx); return; } + ToastTarget::Activate(id) => (id, ShellNotificationsAction::Activated(id)), + ToastTarget::Dismiss(id) => (id, ShellNotificationsAction::Dismissed(id)), + ToastTarget::Action(id) => (id, ShellNotificationsAction::Action(id)), + }; + cx.widget_action(self.uid, action); + self.dismiss(cx, id); + } + + fn touch_event(&mut self, cx: &mut Cx, event: &makepad_platform::event::TouchUpdateEvent) { + use makepad_platform::event::TouchState; + self.reconcile_touch(event); + for t in &event.touches { + if self.touch.is_none() && t.state == TouchState::Start { + if let Some(target) = self.target(t.abs) { + self.touch = Some(ToastTouch { uid: t.uid, start: t.abs, target: Some(target) }); + } + } + let Some(held) = self.touch.as_mut().filter(|held| held.uid == t.uid) else { continue }; + if (t.abs - held.start).length() > TOUCH_SLOP { held.target = None; } + if t.state == TouchState::Stop { + let held = self.touch.take().unwrap(); + if let Some(target) = held.target.filter(|target| self.target(t.abs) == Some(*target)) { + self.activate_target(cx, target); + } + } + } + } + /// The width a toast's action button takes (none without one). fn action_width(&mut self, cx: &mut Cx2d, note: &Notification) -> f64 { let tok = self.d.tokens(self.tokens); @@ -373,6 +451,10 @@ impl ShellNotifications { if self.live.is_empty() { return; } + if self.mobile { + self.draw_phone_stack(cx, screen); + return; + } let tok = self.d.tokens(self.tokens); let gaps_out = tok.spacing.gaps_out; let border = tok.notifications.surface.border_width; @@ -537,6 +619,95 @@ impl ShellNotifications { self.logged = layout; } } + + fn draw_phone_stack(&mut self, cx: &mut Cx2d, screen: Rect) { + use super::ui::{phone_card_colors, HAlign}; + let safe = cx.display_context.safe_area_insets; + let bounds = phone_stack_bounds(screen, safe.left, safe.top, safe.right, safe.bottom); + let (fill, foreground) = phone_card_colors(crate::glance_card::dark()); + let now = cx.seconds_since_app_start(); + let scale = self.d.text_scale(); + let (header_h, title_h, body_h) = (24.0 * scale, 21.0 * scale, 18.0 * scale); + let mut y = bounds.pos.y; + let wanted = if self.expanded { self.live.len() } else { self.live.len().min(2) }; + for entry in self.live.clone().into_iter().take(wanted) { + let note = &entry.note; + let text_w = bounds.size.x - 32.0; + let mut title = self.d.wrap(cx, true, 15.0 * scale, ¬e.summary, text_w, 2); + let mut body = self.d.wrap(cx, false, 13.0 * scale, ¬e.body, text_w, 2); + let action_h = if note.action.is_some() { 48.0 } else { 0.0 }; + let height = |titles: usize, lines: usize| 14.0 + header_h + 8.0 + title_h * titles as f64 + + if lines == 0 { 0.0 } else { 3.0 + body_h * lines as f64 } + + 14.0 + action_h; + let available = bounds.pos.y + bounds.size.y - y; + if !self.card_rects.is_empty() && height(title.len(), body.len()) + 54.0 > available { break; } + // In landscape or enlarged text, preserve the title/action and + // shorten only the preview. The full card retains the whole body. + while !body.is_empty() && height(title.len(), body.len()) > available { + if body.len() == 1 { body.clear(); } + else { body = self.d.wrap(cx, false, 13.0 * scale, ¬e.body, text_w, body.len() - 1); } + } + if height(title.len(), body.len()) > available { + title = self.d.wrap(cx, true, 15.0 * scale, ¬e.summary, text_w, 1); + } + let h = height(title.len(), body.len()); + if h > available { break; } + let offset = slide_offset(now - entry.posted, entry.left, entry.lifetime); + let opacity = (1.0 - offset / SLIDE_PX).clamp(0.0, 1.0) as f32; + let card = rect(bounds.pos.x, y - offset * 0.35, bounds.size.x, h); + let ink = fade(foreground, opacity); + self.d.phone_card(cx, card, fill, opacity); + let icon = rect(card.pos.x + 16.0, card.pos.y + 14.0, 20.0, 20.0); + if let Some(app) = note.app_icon.as_deref() { + self.app_icons.draw(cx, app, self.icon_style, icon, opacity, ink); + } else if let Some(icon_kind) = note.icon { + self.d.icon_centered(cx, icon_kind, icon, 16.0, ink); + } + let caption = if note.caption.is_empty() { "Notification" } else { ¬e.caption }; + self.d.label_elided(cx, rect(icon.pos.x + 28.0, icon.pos.y, text_w - 66.0, header_h), + false, 11.5, fade(ink, 0.68), HAlign::Left, caption); + // A visible close affordance with a 44-point touch target. + let close = rect(card.pos.x + card.size.x - 48.0, card.pos.y + 2.0, 44.0, 44.0); + self.d.icon_centered(cx, Ico::Close, close, 12.0, fade(ink, 0.65)); + let mut ty = card.pos.y + 14.0 + header_h + 8.0; + for line in &title { + self.d.label(cx, rect(card.pos.x + 16.0, ty, text_w, title_h), true, 15.0, ink, HAlign::Left, line); + ty += title_h; + } + ty += 3.0; + for line in &body { + self.d.label(cx, rect(card.pos.x + 16.0, ty, text_w, body_h), false, 13.0, fade(ink, 0.78), HAlign::Left, line); + ty += body_h; + } + if let Some(label) = ¬e.action { + let action = rect(card.pos.x + 12.0, card.pos.y + h - 52.0, card.size.x - 24.0, 44.0); + self.d.rounded(cx, action, 12.0, fade(ink, 0.06)); + self.d.label_elided(cx, action, true, 13.0, ink, HAlign::Center, label); + self.action_rects.push((note.id, action)); + } + self.card_rects.push((note.id, card, close)); + y += h + 10.0; + } + let hidden = self.live.len() - self.card_rects.len(); + if hidden > 0 && y + 44.0 <= bounds.pos.y + bounds.size.y { + self.more = rect(bounds.pos.x + bounds.size.x - 116.0, y, 116.0, 44.0); + self.d.phone_card(cx, self.more, fill, 1.0); + self.d.label(cx, self.more, true, 12.0, foreground, HAlign::Center, &format!("{hidden} more")); + } + let layout = self.card_rects.iter().map(|(id, r, _)| format!("{id}@{},{},{},{}", + r.pos.x as i32, r.pos.y as i32, r.size.x as i32, r.size.y as i32)).collect::>().join(" "); + if layout != self.logged { + log!("notifications: {} toast(s) {layout}", self.card_rects.len()); + self.logged = layout; + } + } +} + +fn phone_stack_bounds(screen: Rect, left: f64, top: f64, right: f64, bottom: f64) -> Rect { + let available = (screen.size.x - left - right - 24.0).max(0.0); + let width = available.min(420.0); + rect(screen.pos.x + left + 12.0 + (available - width) * 0.5, + screen.pos.y + top + 8.0, width, (screen.size.y - top - bottom - 24.0).max(0.0)) } impl Widget for ShellNotifications { @@ -549,7 +720,9 @@ impl Widget for ShellNotifications { } fn handle_event(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) { + if matches!(event, Event::Pause | Event::Background) { self.touch = None; } if self.inert { + self.touch = None; return; } if let Some(ne) = self.next_frame.is_event(event) { @@ -581,6 +754,7 @@ impl Widget for ShellNotifications { } } match event { + Event::TouchUpdate(e) => self.touch_event(cx, e), Event::MouseMove(e) => { let rects = self.card_rects.clone(); let mut changed = false; @@ -673,6 +847,105 @@ pub fn fixtures() -> Vec { mod tests { use super::*; + #[test] + fn phone_notifications_clear_status_bar_and_both_screen_edges() { + for (w, h) in [(320.0, 640.0), (384.0, 810.0), (810.0, 384.0)] { + let r = phone_stack_bounds(rect(0.0, 0.0, w, h), 8.0, 32.0, 24.0, 24.0); + assert!(r.pos.x >= 20.0 && r.pos.y >= 40.0); + assert!(r.pos.x + r.size.x <= w - 36.0); + assert!(r.pos.y + r.size.y <= h - 24.0); + assert!(r.size.x <= 420.0); + } + } + + fn touch(state: makepad_platform::event::TouchState, abs: Vec2d) -> Event { + use makepad_platform::event::{TouchPoint, TouchUpdateEvent}; + Event::TouchUpdate(TouchUpdateEvent { + time: 0., window_id: CxWindowPool::id_zero(), modifiers: Default::default(), + touches: vec![TouchPoint { state, abs, time: 0., uid: 7, rotation_angle: 0., force: 0., + radius: dvec2(1., 1.), handled: Default::default(), sweep_lock: Default::default() }], + }) + } + fn toast(cx: &mut Cx) -> (ShellNotifications, u64) { + let mut notes = cx.with_vm(ShellNotifications::script_new); + // Input regression: seed an already posted notice without starting a + // native platform clock/window just to exercise its touch handler. + let id = 1; + let mut note = fixtures()[0].clone(); + note.id = id; + notes.live.push(Live { note, lifetime: 30., left: 30., hovered: false, posted: 0. }); + notes.next_id = id; + notes.card_rects = vec![(id, rect(10., 10., 300., 100.), rect(280., 10., 30., 30.))]; + (notes, id) + } + fn deliver(notes: &mut ShellNotifications, cx: &mut Cx, event: Event) -> Vec { + cx.capture_actions(|cx| notes.handle_event(cx, &event, &mut Scope::empty())) + .iter().filter_map(|a| a.as_widget_action()).map(|a| a.cast()).collect() + } + #[test] + fn phone_toast_tap_owns_press_and_release_and_emits_one_activation() { + use makepad_platform::event::TouchState::*; + let mut cx = Cx::new(Box::new(|_, _| {})); + let (mut notes, id) = toast(&mut cx); + let down = touch(Start, dvec2(40., 50.)); + assert!(notes.owns_pointer(&down)); + assert!(deliver(&mut notes, &mut cx, down).is_empty()); + let up = touch(Stop, dvec2(42., 51.)); + assert!(notes.owns_pointer(&up), "release must not reach the underlying chat"); + assert_eq!(deliver(&mut notes, &mut cx, up), vec![ShellNotificationsAction::Activated(id)]); + assert!(notes.is_empty()); + assert!(!notes.owns_pointer(&touch(Stop, dvec2(42., 51.)))); + } + #[test] + fn phone_toast_drag_is_cancelled_but_keeps_its_release_out_of_underlying_ui() { + use makepad_platform::event::TouchState::*; + let mut cx = Cx::new(Box::new(|_, _| {})); + let (mut notes, _) = toast(&mut cx); + let mut underlying_events = 0; + for event in [touch(Start, dvec2(40., 50.)), touch(Move, dvec2(40., 180.)), touch(Stop, dvec2(40., 50.))] { + // This is the shell's early routing decision, not a broadcast. + if notes.owns_pointer(&event) { assert!(deliver(&mut notes, &mut cx, event).is_empty()); } + else { underlying_events += 1; } + } + assert_eq!(underlying_events, 0); + assert!(!notes.is_empty(), "returning after a drag is not a tap"); + assert!(notes.touch.is_none()); + } + #[test] + fn phone_toast_close_dismisses_and_a_withdrawn_target_cannot_activate() { + use makepad_platform::event::TouchState::*; + let mut cx = Cx::new(Box::new(|_, _| {})); + let (mut notes, id) = toast(&mut cx); + deliver(&mut notes, &mut cx, touch(Start, dvec2(295., 25.))); + assert_eq!(deliver(&mut notes, &mut cx, touch(Stop, dvec2(295., 25.))), vec![ShellNotificationsAction::Dismissed(id)]); + let (mut notes, id) = toast(&mut cx); + deliver(&mut notes, &mut cx, touch(Start, dvec2(40., 50.))); + notes.dismiss(&mut cx, id); + let up = touch(Stop, dvec2(40., 50.)); + assert!(notes.owns_pointer(&up)); + assert!(deliver(&mut notes, &mut cx, up).is_empty()); + } + + #[test] + fn a_modal_consuming_release_cannot_leave_the_toast_capturing_future_touches() { + use makepad_platform::event::TouchState::*; + let mut cx = Cx::new(Box::new(|_, _| {})); + for next_uid in [7, 8] { + let (mut notes, _) = toast(&mut cx); + deliver(&mut notes, &mut cx, touch(Start, dvec2(40., 50.))); + // An approval above the toast receives Stop instead. The next + // gesture must reach its actual target, even if Android reuses ID. + let mut outside = touch(Start, dvec2(40., 300.)); + if let Event::TouchUpdate(e) = &mut outside { e.touches[0].uid = next_uid; } + assert!(!notes.owns_pointer(&outside)); + assert!(notes.touch.is_none()); + let down = touch(Start, dvec2(40., 50.)); + assert!(notes.owns_pointer(&down)); + assert!(deliver(&mut notes, &mut cx, down).is_empty()); + assert_eq!(deliver(&mut notes, &mut cx, touch(Stop, dvec2(40., 50.))).len(), 1); + } + } + /// A toast slides in from the right over its first [`SLIDE_S`], keeps /// still, and slides out over its last; one that never expires never /// slides out. (Unless `OCTOSENSE_REDUCE_MOTION` is set for the test diff --git a/crates/shell/src/shell/ui.rs b/crates/shell/src/shell/ui.rs index 2f7bae5e..17a45833 100644 --- a/crates/shell/src/shell/ui.rs +++ b/crates/shell/src/shell/ui.rs @@ -43,6 +43,23 @@ script_mod! { } } + // Flat phone surfaces keep rounded corners without invoking the desktop + // glass compositor or inheriting a window's accent border. + set_type_default() do #(DrawShellRound::script_shader(vm)) { + ..mod.draw.DrawQuad + color: #ffffff + radius: 18.0 + pixel: fn() { + let p = self.pos * self.rect_size + let half = self.rect_size * 0.5 + let radius = min(self.radius, min(half.x, half.y)) + let q = abs(p - half) - half + vec2(radius) + let d = length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - radius + let a = clamp(0.5 - d, 0.0, 1.0) * self.color.w + return vec4(self.color.rgb * a, a) + } + } + // `BorderSurface`: a fill plus a hard square ring measured straight off // the quad edges. The ring takes two stops and an angle so a theme's // hyprland `active-border` gradient (what `[popups] border` resolves to) @@ -594,6 +611,17 @@ pub struct DrawShellFill { pub color: Vec4f, } +#[derive(Script, ScriptHook)] +#[repr(C)] +pub struct DrawShellRound { + #[deref] + draw_super: DrawQuad, + #[live] + color: Vec4f, + #[live(18.0)] + radius: f32, +} + #[derive(Script, ScriptHook)] #[repr(C)] pub struct DrawShellChrome { @@ -897,6 +925,8 @@ pub struct ShellDraw { #[live] pub fill: DrawShellFill, #[live] + round: DrawShellRound, + #[live] pub chrome: DrawShellChrome, #[live] pub ring: DrawShellRing, @@ -959,6 +989,13 @@ pub fn rect(x: f64, y: f64, w: f64, h: f64) -> Rect { } } +/// Phone overlays follow the card's light/dark appearance, rather than a +/// desktop window-manager theme that may have the opposite appearance. +pub fn phone_card_colors(dark: bool) -> (Vec4f, Vec4f) { + if dark { (super::rgb(28, 30, 36), super::rgb(245, 245, 247)) } + else { (super::rgb(255, 255, 255), super::rgb(28, 28, 30)) } +} + /// Shrink a rect on every side (QML `anchors.margins`). pub fn inset(r: Rect, by: f64) -> Rect { rect( @@ -1398,6 +1435,20 @@ impl ShellDraw { self.fill.draw_abs(cx, r); } + pub fn rounded(&mut self, cx: &mut Cx2d, r: Rect, radius: f64, color: Vec4f) { + self.round.color = color; + self.round.radius = radius as f32; + self.round.draw_abs(cx, r); + } + + /// A quiet, opaque phone card. Its inset shadow stays independent of + /// desktop window borders; the caller supplies the current appearance. + pub fn phone_card(&mut self, cx: &mut Cx2d, r: Rect, fill: Vec4f, opacity: f32) { + self.rounded(cx, rect(r.pos.x - 1.0, r.pos.y + 3.0, r.size.x + 2.0, r.size.y + 2.0), 21.0, + alpha(super::rgb(0, 0, 0), 0.06 * opacity)); + self.rounded(cx, r, 20.0, fade(fill, opacity)); + } + /// `BorderSurface`: fill + ring. `border_end`/`angle` let a theme's /// hyprland gradient through. pub fn bordered( @@ -2282,4 +2333,3 @@ mod wrap_tests { assert_eq!(wrap_with("one\r\ntwo\n", 10.0, true, width), ["one", "two", ""], "a trailing break is a new, empty line"); } } - diff --git a/docs/testing/evidence/agent-card-polish/01-notification-over-chat.png b/docs/testing/evidence/agent-card-polish/01-notification-over-chat.png new file mode 100644 index 00000000..d7a98279 Binary files /dev/null and b/docs/testing/evidence/agent-card-polish/01-notification-over-chat.png differ diff --git a/docs/testing/evidence/agent-card-polish/02-full-card.png b/docs/testing/evidence/agent-card-polish/02-full-card.png new file mode 100644 index 00000000..19b147b6 Binary files /dev/null and b/docs/testing/evidence/agent-card-polish/02-full-card.png differ diff --git a/docs/testing/evidence/agent-card-polish/03-glance-after-close.png b/docs/testing/evidence/agent-card-polish/03-glance-after-close.png new file mode 100644 index 00000000..4b370ad6 Binary files /dev/null and b/docs/testing/evidence/agent-card-polish/03-glance-after-close.png differ diff --git a/docs/testing/evidence/agent-card-polish/agent-evidence.json b/docs/testing/evidence/agent-card-polish/agent-evidence.json new file mode 100644 index 00000000..2d244700 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/agent-evidence.json @@ -0,0 +1,98 @@ +[ + { + "tool_calls": [ + { + "turn_id": "01a100d0-3d07-7e93-ae1c-5608fb45b449", + "tool_call_id": "call_00_ET_VdAg07o7CSDFTnfkGKjJ6544", + "name": "mail_notify", + "arguments": { + "body": "Synthetic test: the system agent asked the Mail agent to create this card. Tap the notification to read the full card; it should also appear in Glance.", + "card_id": "mail-card-polish-1791014993", + "priority": 70, + "title": "Mail agent E2E" + }, + "input_complete": true, + "start_seq": 1322, + "success": true, + "output": "{\"card_id\":\"mail-card-polish-1791014993\",\"expires_at\":1791101410685,\"replaced\":false}", + "output_is_preview": true, + "end_seq": 1324 + } + ], + "assistant_finals": [ + { + "turn_id": "01a100d0-3d07-7e93-ae1c-5608fb45b449", + "message_id": "_main:api:octosense#peer-os-mail-f6e8245bc1167c11:31:1791015012030475264", + "text": "Success.\n\n- **Outcome:** success\n- **card_id:** `mail-card-polish-1791014993`\n- **Replaced existing card:** No — `\"replaced\": false`\n- **Priority:** 70\n- **Expires at:** 1791101410685 (epoch ms)\n\nThe card is published and should appear in Glance.", + "terminal_verified": true, + "source": "ledger_terminal_message_id" + } + ], + "turn_errors": [ + { + "turn_id": "01a100ce-9df5-7873-bcee-4a513c6b3216", + "code": "orphaned_by_restart", + "message": "server restarted before this turn finished" + } + ], + "session": "_main:api:octosense#peer-os-mail-f6e8245bc1167c11" + }, + { + "tool_calls": [ + { + "turn_id": "31c38ecd-e242-4bb8-974a-ff6befd510fe", + "tool_call_id": "call_00_K2B0Sk4wksaywbPWF8oV1167", + "name": "peer_list", + "arguments": {}, + "input_complete": true, + "start_seq": 229367, + "success": true, + "output": "peers (6):\n- os.camera 22a12f906cd27b73 (os-camera-22a12f906cd27b73) running updated — turns 0\n · exec=?\n- os.mail f6e8245bc1167c11 (os-mail-f6e8245bc1167c11) done updated 1791007606 turns 7...", + "output_is_preview": true, + "end_seq": 229369 + }, + { + "turn_id": "31c38ecd-e242-4bb8-974a-ff6befd510fe", + "tool_call_id": "call_00_ET_aUJntbNnpBeoQbRaWU1n3397", + "name": "peer_send_input", + "arguments": { + "slug": "os-mail-f6e8245bc1167c11", + "message": "Please publish a synthetic notification card via your mail.notify tool (do not read messages or send email):\n- title: \"Mail agent E2E\"\n- body: \"Synthetic test: the system agent asked the Mail agent to create this card. Tap the notification to read the full card; it should also appear in Glance.\"\n- card_id: \"mail-card-polish-1791014993\"\n- priority: 70\n\nThis is the isolated demo-mail test account. Reply with the real outcome and the card_id." + }, + "input_complete": true, + "start_seq": 229376, + "success": true, + "output": "message sent to peer os-mail-f6e8245bc1167c11 — the peer will process it as its next turn", + "output_is_preview": true, + "end_seq": 229378 + }, + { + "turn_id": "31c38ecd-e242-4bb8-974a-ff6befd510fe", + "tool_call_id": "call_00_ET_8aEeKUUYVVAgOYtjN0yk8747", + "name": "peer_gather", + "arguments": { + "slugs": [ + "os-mail-f6e8245bc1167c11" + ] + }, + "input_complete": true, + "start_seq": 229386, + "success": true, + "output": "## peer os.mail f6e8245bc1167c11 [os-mail-f6e8245bc1167c11] (done)\nBrief: You are the os.mail app's assistant. You work for the os.mail app inside its own workspace and memory, and you answer to the s...", + "output_is_preview": true, + "end_seq": 229388 + } + ], + "assistant_finals": [ + { + "turn_id": "31c38ecd-e242-4bb8-974a-ff6befd510fe", + "message_id": "_main:api:octosense#system:893:1791015016239508804", + "text": "Done — delegated to Mail's agent; no direct `mail.notify`, no message reading, no email.\n\n## Real outcome\n- **Consent:** Mail was already `allowed` (peer `os-mail-f6e8245bc1167c11`); no `agents_ask` needed.\n- **Delivery:** `peer_send_input` to that slug.\n- **Mail agent's reply** (`peer_gather`, outcome `completed`, turn 8):\n - **Outcome:** success\n - **card_id:** `mail-card-polish-1791014993`\n - **Replaced existing card:** No — `\"replaced\": false`\n - **Priority:** 70\n - **Expires at:** `1791101410685` (epoch ms)\n - Its note: the card is published and should appear in Glance.\n\nSo the card_id is `mail-card-polish-1791014993`.\n\n**Not claimed:** I did not observe any notification banner, glance entry, or tap — no visual check. That's for you to do separately; I only have the Mail agent's reported result.", + "terminal_verified": true, + "source": "ledger_terminal_message_id" + } + ], + "turn_errors": [], + "session": "_main:api:octosense#system" + } +] diff --git a/docs/testing/evidence/agent-card-polish/build.json b/docs/testing/evidence/agent-card-polish/build.json new file mode 100644 index 00000000..5679a9c2 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/build.json @@ -0,0 +1,64 @@ +{ + "schema_version": 1, + "variant": "standalone", + "development": true, + "dev_mode": true, + "home_package": "dev.makepad.octosense.studio", + "home_version_code": 2026100306, + "source_revision": "ccf8013f2bd7adbb6c20d5f52f47bcfbcbb55313", + "source_dirty": false, + "runtime": { + "schema_version": 1, + "url": "https://github.com/OctoSense-org/Octoscript-Makepad.git", + "revision": "2cc5ef37d7d6a3d2992673389ce74488f7bb2d87" + }, + "runtime_patches": { + "schema_version": 1, + "makepad": { + "base_revision": "c155f61d0e1600d2ec474209374444a38a09a470", + "patch": "tools/runtime-patches/makepad-settings.patch", + "sha256": "b17352ebf59e50ab3db6ff1cb825c4e006bcdff47d83522e372b8fe585e57570", + "tree": "3eadf628ee75eb4c9b672446bf14c7b95d9f0f5b", + "product_fixes": [ + "Settings integration: Android IME synchronization, accessibility and reduced motion, GLES program-cache validation, obscured-gesture cancellation and scroll-position observation.", + "Contained-app isolation and secret-input restrictions come from the pinned upstream runtime; the Settings overlay preserves those gates.", + "System Back callback owners suppress compatibility Back keys to avoid duplicate navigation on Android 13+.", + "A cancelled search's keyboard hides once: an editor session going inactive retires its input connection without restarting input, which brought the keyboard back empty while it was hiding (Android 11).", + "Windows remote grabs send raw RGBA pixels to the bounded PNG worker before presenting, keeping full-size encoding off the UI thread and scaling before encoding.", + "Windows frame completion: backport Makepad work c508ab0c8 GPU completion polls around native and hosted repaint so upload buffers can retire." + ], + "stacked": [ + { + "patch": "tools/runtime-patches/makepad-system-back.patch", + "sha256": "3268cb486bbd05db7bb6920c26007e437017b6234a4d9f045a99a7d1954b7427" + }, + { + "patch": "tools/runtime-patches/makepad-ime-inactive-no-restart.patch", + "sha256": "8ae6a6aceb93a013234c724ffdc6efc614cc8b9dddd7cae6ed8b1c569b94fdf1" + }, + { + "patch": "tools/runtime-patches/makepad-windows-remote-capture.patch", + "sha256": "2c0fabb6f5c0733d9d3356cde4cc7c8d40aabebf9a4752740c94f332b7e1317a" + }, + { + "patch": "tools/runtime-patches/makepad-windows-frame-completion.patch", + "sha256": "8d58e16bc12aae5f4313fdc5c9d3f6944b96ee20e609fe5ff069fe2403954595" + } + ] + } + }, + "octos_kernel": { + "source": "prebuilt", + "sha256": "4bcfa4f5c60f7e8cad526b49481d40bf0556ce4853aad9c72f853ad866ded1e6" + }, + "artifacts": { + "OctoSenseHome.apk": { + "sha256": "d1dbebdf602f01ee427d68109c500e607b7a5c50fc3f387d5a7c46a1f7b4f882", + "certificate_sha256": "e96d983997281700b9d0ecc674ec11edec96d433c0fa9ddd5c216479f2b738c9" + }, + "OctoSenseBridge.apk": { + "sha256": "2d2143f2f0d21a9400a5587a0af0e3d7dd31f458db7cc47b244a9b32eaabc92c", + "certificate_sha256": "e96d983997281700b9d0ecc674ec11edec96d433c0fa9ddd5c216479f2b738c9" + } + } +} diff --git a/docs/testing/evidence/agent-card-polish/cleanup.json b/docs/testing/evidence/agent-card-polish/cleanup.json new file mode 100644 index 00000000..43e4c6e8 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/cleanup.json @@ -0,0 +1,9 @@ +{ + "provider_profile_modified_by_wrapper": false, + "consent_records_modified_by_wrapper": false, + "production_home_touched": false, + "automated_flow_passed": true, + "visual_review": "pending", + "developer_state_restored_exactly": true, + "test_package_stopped": true +} diff --git a/docs/testing/evidence/agent-card-polish/display.json b/docs/testing/evidence/agent-card-polish/display.json new file mode 100644 index 00000000..27f1c06d --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/display.json @@ -0,0 +1,11 @@ +{ + "screen_width_px": 1080, + "screen_height_px": 2280, + "density_dpi": 450, + "layout_scale": 2.8125, + "expected_card_width_px": 967.5, + "min_card_height_px": 202.5, + "max_card_height_px": 1237.5, + "width_tolerance_px": 4, + "assumption": "portrait full-window layout; no Makepad DPI override" +} diff --git a/docs/testing/evidence/agent-card-polish/final-cleanup.json b/docs/testing/evidence/agent-card-polish/final-cleanup.json new file mode 100644 index 00000000..b69f3147 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/final-cleanup.json @@ -0,0 +1,27 @@ +{ + "scope": "Temporary isolated test packages only; source/evidence retained", + "original_provider_profiles_absent": true, + "original_developer_grants_absent": true, + "packages": [ + { + "package": "dev.makepad.octosense.studio", + "temporary_provider_configuration_removed": true, + "temporary_developer_grants_removed": true, + "test_package_stopped": true + }, + { + "package": "dev.makepad.octosense.studio.minimax", + "temporary_provider_configuration_removed": true, + "temporary_developer_grants_removed": true, + "test_package_stopped": true + } + ], + "production_home_modified": false, + "rom_modified": false, + "default_launcher_modified": false, + "network_policy_modified": false, + "screen_timeout_ms": 60000, + "screen_timeout_restored": true, + "adb_shell_unprivileged": true, + "recorded_at_utc": "2026-10-03T08:12:38Z" +} diff --git a/docs/testing/evidence/agent-card-polish/foreground.json b/docs/testing/evidence/agent-card-polish/foreground.json new file mode 100644 index 00000000..ea4ecf8a --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/foreground.json @@ -0,0 +1,6 @@ +{ + "awake": true, + "test_activity_foreground": true, + "keyguard_dismiss_requested": true, + "keyguard_positive_flag_seen": false +} diff --git a/docs/testing/evidence/agent-card-polish/installed-build.json b/docs/testing/evidence/agent-card-polish/installed-build.json new file mode 100644 index 00000000..addde001 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/installed-build.json @@ -0,0 +1,7 @@ +{ + "package": "dev.makepad.octosense.studio", + "source_revision": "ccf8013f2bd7adbb6c20d5f52f47bcfbcbb55313", + "installed_apk_sha256": "d1dbebdf602f01ee427d68109c500e607b7a5c50fc3f387d5a7c46a1f7b4f882", + "matches_local_receipt": true, + "exact_polish_build": false +} diff --git a/docs/testing/evidence/agent-card-polish/notification-route.log b/docs/testing/evidence/agent-card-polish/notification-route.log new file mode 100644 index 00000000..1794a708 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/notification-route.log @@ -0,0 +1,9 @@ +10-02 21:10:10.697 27953 27974 I Makepad : crates/shell/src/lib.rs:4288:13 - glance: toast 1 opens os.mail/mail-card-polish-1791014993 +10-02 21:10:10.697 27953 27974 I Makepad : crates/shell/src/lib.rs:4295:9 - glance: os.mail notifies os.mail/mail-card-polish-1791014993 +10-02 21:10:10.963 27953 27974 I Makepad : crates/shell/src/shell/notifications.rs:700:13 - notifications: 1 toast(s) 1@12,35,360,120 +10-02 21:10:10.975 27953 27974 I Makepad : crates/shell/src/shell/notifications.rs:700:13 - notifications: 1 toast(s) 1@12,36,360,120 +10-02 21:10:12.712 27953 27974 I Makepad : crates/shell/src/glance_sheet.rs:147:9 - glance sheet: opened os.mail/mail-card-polish-1791014993 +10-02 21:10:12.712 27953 27974 I Makepad : crates/shell/src/lib.rs:3875:13 - wm: glance toast opens card os.mail/mail-card-polish-1791014993 +10-02 21:10:13.273 27953 27974 I Makepad : crates/shell/src/glance_sheet.rs:224:17 - glance sheet: os.mail/mail-card-polish-1791014993 sheet@12,386,360,384 card@24,438,336,320 close@342,412 +10-02 21:10:13.290 27953 27974 I Makepad : crates/shell/src/glance_sheet.rs:224:17 - glance sheet: os.mail/mail-card-polish-1791014993 sheet@12,511,360,258 card@24,563,336,194 close@342,537 +10-02 21:10:14.440 27953 27974 I Makepad : crates/shell/src/glance_sheet.rs:155:13 - glance sheet: closed os.mail/mail-card-polish-1791014993 diff --git a/docs/testing/evidence/agent-card-polish/observation.json b/docs/testing/evidence/agent-card-polish/observation.json new file mode 100644 index 00000000..7485661e --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/observation.json @@ -0,0 +1,17 @@ +{ + "card_id": "mail-card-polish-1791014993", + "normal_consent_mode": true, + "tap_screen_pixels": [ + 540, + 270 + ], + "publish_seen": true, + "full_card_opened_in_log": true, + "close_screen_pixels": [ + 962, + 1510 + ], + "x_closed_card_in_log": true, + "same_activity_after_close": true, + "system_terminal_seen": true +} diff --git a/docs/testing/evidence/agent-card-polish/prior-attempt.json b/docs/testing/evidence/agent-card-polish/prior-attempt.json new file mode 100644 index 00000000..79f3c3b2 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/prior-attempt.json @@ -0,0 +1,10 @@ +{ + "automated_flow_passed": false, + "failure": "Operator harness searched a stale pre-draw log snapshot for the notification touch rectangle.", + "notification_screenshot_captured": true, + "tap_completed": false, + "developer_state_restored_exactly": true, + "resolution": "Refresh PID-filtered logs and wait up to four seconds for the measured rectangle before tapping; runtime unchanged.", + "claim_limit": "This was a harness failure, not evidence of a production touch regression.", + "prior_harness_sha256": "51ed70f4d71b1f3e58cc5a5aba9cbece454e3dcffedbca4f8361475af9a28c0d" +} diff --git a/docs/testing/evidence/agent-card-polish/sha256.json b/docs/testing/evidence/agent-card-polish/sha256.json new file mode 100644 index 00000000..4b291fe3 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/sha256.json @@ -0,0 +1,78 @@ +{ + "01-notification-over-chat.png": { + "sha256": "4347dd2c1e005eaad72d3c745aa6c871eebfc38342d10fe8d9a161d83b446eb4", + "bytes": 690184, + "dimensions_px": [ + 1080, + 2280 + ] + }, + "02-full-card.png": { + "sha256": "afcfbea457e7e2c8a3dfba6d3c92687b57a74f2853f988d367ecae93a95382e6", + "bytes": 345658, + "dimensions_px": [ + 1080, + 2280 + ] + }, + "03-glance-after-close.png": { + "sha256": "964e250077cb7336b902fe625f5b281488f3b87a3eec3abce8553d8fdba92041", + "bytes": 336421, + "dimensions_px": [ + 1080, + 2280 + ] + }, + "agent-evidence.json": { + "sha256": "cc6b15ba3cdcb4f6fbef43134c2069c2385719a08a9cf38a195fb48029eaa2f3", + "bytes": 5061 + }, + "build.json": { + "sha256": "d7ba85d211046bd6d7b8be88ea5e4976a8446f350c0158eff56f6a15b8ae7926", + "bytes": 3175 + }, + "cleanup.json": { + "sha256": "33edc99ed89a3bd04743fedcb83b83507a90f9aff81e1fe8a42b78f191770705", + "bytes": 275 + }, + "display.json": { + "sha256": "db177c8b8971ea59b2e17c7017d81c9fb48ff69915591c2d5d80515fce00fed4", + "bytes": 303 + }, + "final-cleanup.json": { + "sha256": "1563153e757bc6e72a9d72b09409ce6373696484a1ece8a0b8d790351fa99bed", + "bytes": 884 + }, + "foreground.json": { + "sha256": "fd4eebab0153e4f55c991d2147d0cb5f42dd5f2baf8ad09ea222207acb6e0425", + "bytes": 134 + }, + "installed-build.json": { + "sha256": "21d2b60f28a3d6e377e61ff742b1374e0db77c4a7b25728bad0d7c7e160e6840", + "bytes": 271 + }, + "notification-route.log": { + "sha256": "f0aa25636e67a9e57a4086d552b23bf1f36ec5b9d516aec14049c0f2f42cb1aa", + "bytes": 1344 + }, + "observation.json": { + "sha256": "ca6fee3af97b6cd861b726f16f7bd8e4ed35a7e8860be3469283fd76a047b8fb", + "bytes": 335 + }, + "prior-attempt.json": { + "sha256": "8f16fc9411d1dfb26d0f5b7b55eea02a4e467a367d2d84e0adbe031ac36e91ed", + "bytes": 585 + }, + "system-prompt.txt": { + "sha256": "604344864fb4af85ee56a9fc0a19efce70809b3e19f4e34c82e93562b31c9619", + "bytes": 838 + }, + "validation-scope.json": { + "sha256": "549b84f135e0fca92a49f0e02e40539495f848e6265040525145b435a862b39d", + "bytes": 896 + }, + "visual-review.json": { + "sha256": "188634eb85c87eabf1aa6e2a3d7d6ed61d2a6bf3c9413dd8752d7d8e54a9762f", + "bytes": 789 + } +} diff --git a/docs/testing/evidence/agent-card-polish/system-prompt.txt b/docs/testing/evidence/agent-card-polish/system-prompt.txt new file mode 100644 index 00000000..8ccc9e95 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/system-prompt.txt @@ -0,0 +1 @@ +Run a real local notification test by delegating to Mail's app agent. Use agents.list, then agents.ask for os.mail if consent is needed; wait for the on-screen consent. Use its actual peer_slug with peer_send_input. Ask Mail's agent to publish a synthetic notice through its mail.notify tool with title "Mail agent E2E", body "Synthetic test: the system agent asked the Mail agent to create this card. Tap the notification to read the full card; it should also appear in Glance.", card_id "mail-card-polish-1791014993", priority 70. Do not call mail.notify yourself. Do not read messages, send email, or use terminal commands. This is the isolated demo-mail test account. Gather the Mail agent's reply using peer_gather and report the real outcome/card_id. Do not claim any screen tap or visual check; the tester will do those separately. \ No newline at end of file diff --git a/docs/testing/evidence/agent-card-polish/validation-scope.json b/docs/testing/evidence/agent-card-polish/validation-scope.json new file mode 100644 index 00000000..33b3b784 --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/validation-scope.json @@ -0,0 +1,17 @@ +{ + "package": "dev.makepad.octosense.studio", + "source_revision": "ccf8013f2bd7adbb6c20d5f52f47bcfbcbb55313", + "source_dirty": false, + "polish_commit_included": "9e65b3bc8f944fbd5c21fe6cca3215fe7ed4d915", + "exact_polish_build": false, + "scope": "Composite integration and visual validation of PR314 polish", + "normal_consent_mode": true, + "first_use_consent_tested": false, + "consent": "Existing manual consent reused; consent records untouched", + "provider": "Existing authorized test-package provider profile untouched", + "mail": "Existing synthetic demo account; mail_demo enabled", + "production_home_touched": false, + "harness_sha256": "5995fab6a63783fe5312da65ac3c1789f05bc4e1725d887902a054c8d39da300", + "wrapper_sha256": "08c7439b6c08f5fc9a3db3249d75ca4fb6647c8ebc85408188e24e12fbb7d2ac", + "visual_review": "Pending; all three fresh screenshots require independent review" +} diff --git a/docs/testing/evidence/agent-card-polish/visual-review.json b/docs/testing/evidence/agent-card-polish/visual-review.json new file mode 100644 index 00000000..5a2c559c --- /dev/null +++ b/docs/testing/evidence/agent-card-polish/visual-review.json @@ -0,0 +1,14 @@ +{ + "review": "Independent operator screenshot review", + "result": "passed for the recorded portrait flow", + "screens": { + "01-notification-over-chat.png": "Rounded banner readable above chat, positioned below status icons; body intentionally elided.", + "02-full-card.png": "Bottom card shows complete title/body, visible close control and intact margins; no observed content overlap.", + "03-glance-after-close.png": "Same title/body visible in undimmed Glance after close." + }, + "limits": [ + "Only this portrait synthetic notice was visually reviewed.", + "Glance status-bar white icons on a light background have low contrast; this existing shell issue remains.", + "Older Calendar-model output visible behind the toast is unrelated to this notification test." + ] +} diff --git a/docs/testing/evidence/agent-card/01-notification-over-chat.png b/docs/testing/evidence/agent-card/01-notification-over-chat.png new file mode 100644 index 00000000..5a9f5305 Binary files /dev/null and b/docs/testing/evidence/agent-card/01-notification-over-chat.png differ diff --git a/docs/testing/evidence/agent-card/02-full-card.png b/docs/testing/evidence/agent-card/02-full-card.png new file mode 100644 index 00000000..3108c814 Binary files /dev/null and b/docs/testing/evidence/agent-card/02-full-card.png differ diff --git a/docs/testing/evidence/agent-card/03-glance-after-close.png b/docs/testing/evidence/agent-card/03-glance-after-close.png new file mode 100644 index 00000000..5bb86981 Binary files /dev/null and b/docs/testing/evidence/agent-card/03-glance-after-close.png differ diff --git a/docs/testing/evidence/agent-card/agent-evidence.json b/docs/testing/evidence/agent-card/agent-evidence.json new file mode 100644 index 00000000..7fe4b620 --- /dev/null +++ b/docs/testing/evidence/agent-card/agent-evidence.json @@ -0,0 +1,92 @@ +[ + { + "tool_calls": [ + { + "turn_id": "01a1002b-9994-70e0-923a-b54dd32731e8", + "tool_call_id": "call_00_ET_2HkTuI1lRBMqWCnajj4n2310", + "name": "mail_notify", + "arguments": { + "body": "Synthetic test: the system agent asked the Mail agent to create this card. Tap the notification to read the full card; it should also appear in Glance.", + "card_id": "agent-card-e2e-accepted", + "priority": 70, + "title": "Mail agent E2E" + }, + "input_complete": true, + "start_seq": 881, + "success": true, + "output": "{\"card_id\":\"agent-card-e2e-accepted\",\"expires_at\":1791090621126,\"replaced\":false}", + "output_is_preview": true, + "end_seq": 883 + } + ], + "assistant_finals": [ + { + "turn_id": "01a1002b-9994-70e0-923a-b54dd32731e8", + "message_id": "_main:api:octosense#peer-os-mail-f6e8245bc1167c11:19:1791004222392096724", + "text": "Reported outcome:\n\n- **Outcome:** success\n- **Replaced existing card:** No — `\"replaced\": false`\n- **card_id used:** `agent-card-e2e-accepted`\n- **Priority:** 70\n- **Expires at:** 1791090621126 (epoch ms)\n\nFresh card_id, so `replaced: false` is expected here — same as the `agent-card-e2e-final` case. The card is published and should be visible in Glance.", + "terminal_verified": true, + "source": "ledger_terminal_message_id" + } + ], + "turn_errors": [], + "session": "_main:api:octosense#peer-os-mail-f6e8245bc1167c11" + }, + { + "tool_calls": [ + { + "turn_id": "66a80d1f-4471-4c41-abf7-9985d8a39767", + "tool_call_id": "call_00_Lvw9RR9sc84nV8vOwrei1207", + "name": "peer_send_input", + "arguments": { + "message": "Please publish a synthetic notification card via your mail.notify tool (do not read messages or send email).\n\nParameters:\n- title: \"Mail agent E2E\"\n- body: \"Synthetic test: the system agent asked the Mail agent to create this card. Tap the notification to read the full card; it should also appear in Glance.\"\n- card_id: \"agent-card-e2e-accepted\"\n- priority: 70\n\nThis is the isolated demo-mail test account. Reply with the real outcome (success/failure), whether it replaced an existing card, and the card_id you used.", + "slug": "os-mail-f6e8245bc1167c11" + }, + "input_complete": true, + "start_seq": 80387, + "success": true, + "output": "message sent to peer os-mail-f6e8245bc1167c11 — the peer will process it as its next turn", + "output_is_preview": true, + "end_seq": 80389 + }, + { + "turn_id": "66a80d1f-4471-4c41-abf7-9985d8a39767", + "tool_call_id": "call_00_ET_zMiLKi7L7duOFUS9OsoR8230", + "name": "peer_list", + "arguments": {}, + "input_complete": true, + "start_seq": 80396, + "success": true, + "output": "peers (6):\n- os.camera 22a12f906cd27b73 (os-camera-22a12f906cd27b73) running updated — turns 0\n · exec=?\n- os.mail f6e8245bc1167c11 (os-mail-f6e8245bc1167c11) done updated 1791003814 turns 4...", + "output_is_preview": true, + "end_seq": 80398 + }, + { + "turn_id": "66a80d1f-4471-4c41-abf7-9985d8a39767", + "tool_call_id": "call_00_Op13f3lOtGH2nBwx5vC68657", + "name": "peer_gather", + "arguments": { + "slugs": [ + "os-mail-f6e8245bc1167c11" + ] + }, + "input_complete": true, + "start_seq": 80473, + "success": true, + "output": "## peer os.mail f6e8245bc1167c11 [os-mail-f6e8245bc1167c11] (done)\nBrief: You are the os.mail app's assistant. You work for the os.mail app inside its own workspace and memory, and you answer to the s...", + "output_is_preview": true, + "end_seq": 80475 + } + ], + "assistant_finals": [ + { + "turn_id": "66a80d1f-4471-4c41-abf7-9985d8a39767", + "message_id": "_main:api:octosense#system:196:1791004224867086254", + "text": "Done — delegated through Mail's agent; no direct `mail.notify`, no message reading, no email, no terminal.\n\n## Real outcome\n\n- **Consent:** Mail already `allowed` this session (peer `os-mail-f6e8245bc1167c11`); no new `agents_ask` needed.\n- **Delivery:** `peer_send_input` to that slug.\n- **Mail agent's reply** (`peer_gather`, outcome `completed`, turn 5):\n - **Outcome:** success\n - **Replaced existing card:** No — `\"replaced\": false`\n - **card_id used:** `agent-card-e2e-accepted`\n - **Priority:** 70\n - **Expires at:** `1791090621126` (epoch ms)\n - Its note: fresh card_id, so `replaced: false` is expected — the card is published and should be visible in Glance.\n\nSo the card_id is `agent-card-e2e-accepted`.\n\n**Not claimed:** I did not observe any notification banner, glance entry, or tap — no visual check. That's yours to do separately; I only have the Mail agent's reported result.", + "terminal_verified": true, + "source": "ledger_terminal_message_id" + } + ], + "turn_errors": [], + "session": "_main:api:octosense#system" + } +] diff --git a/docs/testing/evidence/agent-card/build.json b/docs/testing/evidence/agent-card/build.json new file mode 100644 index 00000000..4106ccd7 --- /dev/null +++ b/docs/testing/evidence/agent-card/build.json @@ -0,0 +1,17 @@ +{ + "source_revision": "c3eaea2ce8bc06192e805438b590f885c870db29", + "source_dirty": false, + "home_package": "dev.makepad.octosense.studio", + "home_version_code": 2026100305, + "dev_mode": true, + "device": "OnePlus 6 / ONEPLUS_A6003", + "provider": "DeepSeek", + "model": "deepseek-v4-flash", + "apk_sha256": "c275722cd60173d13ba490de04425fc0c6c9871acf496fc261df08734b1c60bb", + "packaging_harness_revision": "88550095ec519f1c75b48903a9291f4557c452b2", + "screenshots": { + "01-notification-over-chat.png": "1bf31fb08267bd5f29160127e5e592842a2b3152971c3ab1784724fb8fcf3a15", + "02-full-card.png": "9980528cb596f849b39f39d4b883b3e21147dd3c42870f663e7429f17bc65ae4", + "03-glance-after-close.png": "8c3d7741f28ee38bb7223ec492f4263fb366fd917457136a82a96edeea2e1069" + } +} diff --git a/docs/testing/evidence/agent-card/cleanup.json b/docs/testing/evidence/agent-card/cleanup.json new file mode 100644 index 00000000..fbcd4031 --- /dev/null +++ b/docs/testing/evidence/agent-card/cleanup.json @@ -0,0 +1,6 @@ +{ + "provider_profile_restored": true, + "screen_timeout_restored": true, + "test_package_stopped": true, + "adb_unprivileged": true +} diff --git a/docs/testing/evidence/agent-card/notification-route.log b/docs/testing/evidence/agent-card/notification-route.log new file mode 100644 index 00000000..c357a043 --- /dev/null +++ b/docs/testing/evidence/agent-card/notification-route.log @@ -0,0 +1,8 @@ +10-02 18:10:21.131 21876 21898 I Makepad : crates/shell/src/lib.rs:4245:13 - glance: toast 1 opens os.mail/agent-card-e2e-accepted +10-02 18:10:21.131 21876 21898 I Makepad : crates/shell/src/lib.rs:4252:9 - glance: os.mail notifies os.mail/agent-card-e2e-accepted +10-02 18:10:21.355 21876 21898 I Makepad : crates/shell/src/shell/notifications.rs:612:13 - notifications: 1 toast(s) 1@-1,5,380,119 +10-02 18:10:22.905 21876 21898 I Makepad : crates/shell/src/glance_sheet.rs:130:9 - glance sheet: opened os.mail/agent-card-e2e-accepted +10-02 18:10:22.905 21876 21898 I Makepad : crates/shell/src/lib.rs:3832:13 - wm: glance toast opens card os.mail/agent-card-e2e-accepted +10-02 18:10:23.421 21876 21898 I Makepad : crates/shell/src/glance_sheet.rs:192:17 - glance sheet: os.mail/agent-card-e2e-accepted sheet@16,217,352,376 card@28,261,328,320 close@346,241 +10-02 18:10:23.444 21876 21898 I Makepad : crates/shell/src/glance_sheet.rs:192:17 - glance sheet: os.mail/agent-card-e2e-accepted sheet@16,263,352,282 card@28,307,328,226 close@346,287 +10-02 18:10:24.649 21876 21898 I Makepad : crates/shell/src/glance_sheet.rs:138:13 - glance sheet: closed os.mail/agent-card-e2e-accepted diff --git a/docs/testing/evidence/agent-card/observation.json b/docs/testing/evidence/agent-card/observation.json new file mode 100644 index 00000000..bdc12f41 --- /dev/null +++ b/docs/testing/evidence/agent-card/observation.json @@ -0,0 +1,17 @@ +{ + "card_id": "agent-card-e2e-accepted", + "normal_consent_mode": true, + "tap_screen_pixels": [ + 540, + 175 + ], + "publish_seen": true, + "full_card_opened_in_log": true, + "close_screen_pixels": [ + 973, + 807 + ], + "x_closed_card_in_log": true, + "same_activity_after_close": true, + "system_terminal_seen": true +} diff --git a/docs/testing/evidence/agent-card/system-prompt.txt b/docs/testing/evidence/agent-card/system-prompt.txt new file mode 100644 index 00000000..e85ca355 --- /dev/null +++ b/docs/testing/evidence/agent-card/system-prompt.txt @@ -0,0 +1 @@ +Run a real local notification test by delegating to Mail's app agent. Use agents.list, then agents.ask for os.mail if consent is needed; wait for the on-screen consent. Use its actual peer_slug with peer_send_input. Ask Mail's agent to publish a synthetic notice through its mail.notify tool with title "Mail agent E2E", body "Synthetic test: the system agent asked the Mail agent to create this card. Tap the notification to read the full card; it should also appear in Glance.", card_id "agent-card-e2e-accepted", priority 70. Do not call mail.notify yourself. Do not read messages, send email, or use terminal commands. This is the isolated demo-mail test account. Gather the Mail agent's reply using peer_gather and report the real outcome/card_id. Do not claim any screen tap or visual check; the tester will do those separately. \ No newline at end of file diff --git a/docs/testing/oneplus6-agent-card.md b/docs/testing/oneplus6-agent-card.md new file mode 100644 index 00000000..1139538d --- /dev/null +++ b/docs/testing/oneplus6-agent-card.md @@ -0,0 +1,165 @@ +# OnePlus 6: system agent → Mail card + +English | [简体中文](oneplus6-agent-card.zh-CN.md) + +**Passed on the physical OnePlus 6:** the system agent delegated to Mail's +agent, Mail published a notice, its notification appeared over system chat, +a native finger tap opened the full card, and closing it left the same card +visible in Glance. The accepted card is `os.mail/agent-card-e2e-accepted`. + +The tested runtime was the clean commit +`c3eaea2ce8bc06192e805438b590f885c870db29`, based on main `e6bfad93`. +The [build record](evidence/agent-card/build.json) binds the APK and original +1080×2280 screenshots by SHA-256. This change is independent of App Studio. +Packaging used the separate-package build helper from `88550095`, outside +the checkout with its repository root redirected here; the compiled Rust +source was `c3eaea2c`. The package was `dev.makepad.octosense.studio`. + +## What ran + +Both agent turns used the existing, authorized DeepSeek `deepseek-v4-flash` +configuration. A test operator drove Android touch input over ADB and +reviewed the screenshots; the model did not click or judge the screen. +Mail used its existing `mail_demo` transport and the synthetic identity +`agent-test@example.invalid`, added through the host's sign-in UI. The +recorded turns contain no mail-reading or email-sending tools. + +Developer options were compiled in, but developer grants were absent. +The first attempt exercised the real “Let Mail's agent start?” consent sheet; +the operator tapped Allow. The accepted replay reused that consent and peer. +Production Home, Bridge, the default launcher and ROM were not replaced. + +| Stage | Observed evidence | +| --- | --- | +| System delegates | `peer_send_input` targets `os-mail-f6e8245bc1167c11` with the accepted card ID. | +| Mail publishes | The separate Mail peer calls `mail_notify`; the host returns success, the same ID and `replaced: false`. | +| Notification displays | The Mail banner is visible above the still-open system chat. | +| Native tap opens full card | Android touch at `(540,175)` opens the keyed card sheet; title and complete body are readable. | +| Close returns to Glance | Touching the measured × at `(973,807)` closes the sheet in the same test Activity. Glance shows the same title and body. | +| Reply reaches system agent | `peer_gather` observes the completed Mail peer; both sessions have committed terminal replies. Neither claims to have inspected the screen. | + +[Notification above chat](evidence/agent-card/01-notification-over-chat.png) · +[Full card](evidence/agent-card/02-full-card.png) · +[Glance after closing](evidence/agent-card/03-glance-after-close.png) + +The [sanitized transcript](evidence/agent-card/agent-evidence.json) records +tool arguments, result previews and committed assistant replies, with no +reasoning or provider credentials. The [keyed route log](evidence/agent-card/notification-route.log) +and [touch observations](evidence/agent-card/observation.json) connect the +agent call to the displayed card. The notification screenshot includes an +older reply above the current turn; use the accepted card ID in these +records to identify this run. + +## Repeating the flow + +1. Use an authorized phone with a separate test package and an authorized + provider profile. Launch with `mail_demo: true` in `makepad.APP_CONFIG`. +2. Add the synthetic Mail account through the host sheet, using the demo + transport's public password `demo`. +3. Send the [recorded prompt](evidence/agent-card/system-prompt.txt) to system + chat. The run submitted it through the existing `system-chat-send:` test + action. Let the model obtain consent if needed, delegate to Mail's actual + peer slug, and gather its reply. +4. Keep chat open. Tap the notification, inspect the full card, then tap × + and inspect Glance. Obtain touch targets from the actual screen/layout; + the recorded pixel coordinates are specific to this phone's density. +5. Compare the app-owned card ID across the peer tool result and shell log. + An agent's success reply alone does not establish that a card rendered + or that a tap worked. + +## Bugs found and validation + +Before the fix, chat painted over notifications, and a visible toast ignored +Android touch because its handler accepted only mouse presses. The fix puts +notifications/card sheets above chat, handles matching touch releases, +retains cancelled drags, recovers captures interrupted by a modal, and opens +the exact card with Glance beneath it. Sheet × and backdrop accept touch. + +On `c3eaea2c`, all **838 shell tests passed**, including seven new touch +regressions. Desktop checks with default features and `mobile-apps`, the +Home check with `mobile-apps`, both shell dependency-graph checks and +`tools/setup.py --check --cargo` passed. The physical Android build and +flow above also passed. These unit checks are not a headless UI E2E test; +the interaction evidence comes from the actual phone. + +## Limits and cleanup + +- This tests the **OctoSense in-app toast**, not Android's notification tray, + background delivery or the internal notification shade. +- `mail.notify` supplies content to the shell's existing L0 notice template. + It does not generate new L0 source or launch Mail's full application. +- Calendar, live IMAP/SMTP, card replacement, expiry and persistence across + process restarts were not tested in the accepted run. +- Android Back closed the sheet but also exited the renamed test Activity + in an earlier replay. The pinned Makepad loader looks for + `.MakepadAppExtension`, while Home's class retains its original + package. The test package therefore misses Home's native Back override. + The accepted flow uses ×; normal installed Home's Back behavior remains + **unverified** here. + +After this original run, the temporary provider profile was removed, the original screen timeout +restored, the test package stopped and ADB verified unprivileged. Synthetic +Mail state and test evidence remain for inspection; see the +[cleanup receipt](evidence/agent-card/cleanup.json). + +## Presentation polish: composite-build replay + +**The later portrait replay passed** with card +`os.mail/mail-card-polish-1791014993`. This tests the presentation changes in +`9e65b3bc`: the rounded phone banner, bottom card sheet, larger close target +and revised notice spacing. The installed APK came from clean composite +[`ccf8013f`](https://github.com/OctoSense-org/OctoSense/commit/ccf8013f2bd7adbb6c20d5f52f47bcfbcbb55313), which also contains App Studio. +It is **not an exact-`9e65b3bc` device build**. The +[installed APK fingerprint](evidence/agent-card-polish/installed-build.json) +matches the [composite build receipt](evidence/agent-card-polish/build.json). +Original `c3eaea2c` evidence above remains separate. + +The [new transcript](evidence/agent-card-polish/agent-evidence.json) records +system `peer_send_input` to Mail, the separate peer's successful `mail_notify` +with this card ID and `replaced: false`, then `peer_gather` and both terminal +replies. The [route log](evidence/agent-card-polish/notification-route.log) +and [touch record](evidence/agent-card-polish/observation.json) confirm that +touch `(540,270)` opened that card, and × at `(962,1510)` closed it in the +same Activity. Coordinates use the recorded 450-dpi display. + +[Banner above chat](evidence/agent-card-polish/01-notification-over-chat.png) · +[Full card](evidence/agent-card-polish/02-full-card.png) · +[Glance after close](evidence/agent-card-polish/03-glance-after-close.png) + +Independent screenshot review found readable text, complete full-card body, +visible close control and intact margins, with the same card in Glance after +closing. The banner intentionally elides its body. Glance's white status-bar +icons still have low contrast against its light background; this shell issue +remains. Older Calendar-model output behind the banner belongs to a separate +test. See the [visual review](evidence/agent-card-polish/visual-review.json) +and [artifact hashes](evidence/agent-card-polish/sha256.json). + +The first polish replay stopped because the operator harness searched an old +log snapshot for a not-yet-recorded touch rectangle. Refreshing logs before +target selection fixed the harness; runtime code did not change. The +[failed-attempt summary](evidence/agent-card-polish/prior-attempt.json) +preserves this distinction. The successful transcript also contains an +`orphaned_by_restart` error for an older Mail turn; the accepted publication +and terminal reply belong to a different turn. + +The replay reused existing manual consent and the synthetic demo account. +Developer grants were absent during the flow, then restored to their exact +previous bytes or absence; the test package was stopped. The wrapper left +provider and consent records untouched, as its +[cleanup receipt](evidence/agent-card-polish/cleanup.json) records. +Production Home was untouched. This replay adds no claim about Android Back, +dark mode, landscape, system-tray notifications or live mail. + +After all template and notification tests, the operator separately removed +both isolated packages' temporary provider configurations and developer +grants, restoring their original absence. Both packages were stopped, the +60-second screen timeout restored, and ADB verified unprivileged. This later +[final cleanup](evidence/agent-card-polish/final-cleanup.json) supersedes the +wrapper's temporary grant restoration; it did not change production Home, +the default launcher or ROM. + +Before the composite replay, `9e65b3bc` passed **840 shell tests**, desktop +default/mobile-apps and Home mobile-apps checks, both dependency-graph checks, +setup validation and an Android build. Those checks and this composite +device replay have different source scopes; neither substitutes for an +exact-polish-build device test. diff --git a/docs/testing/oneplus6-agent-card.zh-CN.md b/docs/testing/oneplus6-agent-card.zh-CN.md new file mode 100644 index 00000000..5f96ba92 --- /dev/null +++ b/docs/testing/oneplus6-agent-card.zh-CN.md @@ -0,0 +1,133 @@ +# OnePlus 6:系统 Agent → Mail 卡片 + +[English](oneplus6-agent-card.md) | 简体中文 + +**真机测试通过:**系统 Agent 将任务交给 Mail Agent,后者发布通知卡片; +通知显示在系统聊天上方,手指点击后打开完整卡片,点击 × 后回到 Glance, +同一张卡片仍可见。验收卡片 ID 为 `os.mail/agent-card-e2e-accepted`。 + +测试运行时来自基于 main `e6bfad93` 的干净提交 +`c3eaea2ce8bc06192e805438b590f885c870db29`,与 App Studio 实现独立。 +[构建记录](evidence/agent-card/build.json) 包含 APK 和原始 1080×2280 截图的 +SHA-256。打包使用提交 `88550095` 的独立包名构建辅助脚本,在仓库外运行, +仅将仓库根目录指向当前源码;编译的 Rust 源码仍为 `c3eaea2c`。 +测试包名为 `dev.makepad.octosense.studio`。 + +## 执行过程与证据 + +两个 Agent 都实际调用已获授权复用的 DeepSeek `deepseek-v4-flash` 配置。 +测试人员通过 ADB 发送 Android 触摸输入并检查截图;模型没有执行点击或视觉验收。 +Mail 使用现有 `mail_demo` 传输层,通过宿主登录界面添加测试身份 +`agent-test@example.invalid`。记录中没有读取邮件或发送邮件的工具调用。 + +构建包含开发者选项,但未启用开发者授权。首次尝试出现真实的 +“Let Mail's agent start?” 同意界面,由测试人员点击 Allow;最终重放复用了 +该授权和 peer。生产 Home、Bridge、默认启动器和 ROM 均未替换。 + +| 阶段 | 实际观察 | +| --- | --- | +| 系统 Agent 委派 | `peer_send_input` 将验收卡片任务发给 `os-mail-f6e8245bc1167c11`。 | +| Mail Agent 发布 | 独立 Mail peer 调用 `mail_notify`,返回成功、相同卡片 ID 和 `replaced: false`。 | +| 通知出现 | Mail 通知条显示在仍然打开的系统聊天上方。 | +| 点击打开完整卡片 | Android 原生触摸 `(540,175)` 打开对应卡片,标题和完整正文可读。 | +| 关闭后显示 Glance | 点击测得的 × 位置 `(973,807)`,同一测试 Activity 内关闭弹层,Glance 显示相同标题和正文。 | +| 结果返回系统 Agent | `peer_gather` 取得已完成的 Mail 结果,两个会话均有正式提交的最终回复;均未声称自行检查屏幕。 | + +[聊天上方的通知](evidence/agent-card/01-notification-over-chat.png) · +[完整卡片](evidence/agent-card/02-full-card.png) · +[关闭后的 Glance](evidence/agent-card/03-glance-after-close.png) + +[脱敏会话记录](evidence/agent-card/agent-evidence.json) 仅保留工具参数、结果预览 +和正式回复,不包含推理内容或供应商凭据。 +[路由日志](evidence/agent-card/notification-route.log) 与 +[触摸记录](evidence/agent-card/observation.json) 将工具调用与实际显示关联起来。 +通知截图上方保留了先前轮次的回复;应通过验收卡片 ID 判断当前轮次。 + +## 重复测试 + +1. 在已授权的手机上使用独立测试包和已授权的供应商配置,启动时在 + `makepad.APP_CONFIG` 中设置 `mail_demo: true`。 +2. 通过宿主登录界面添加上述测试身份,使用 demo 传输层的公开口令 `demo`。 +3. 向系统聊天发送[记录的提示词](evidence/agent-card/system-prompt.txt)。本次使用 + 现有 `system-chat-send:` 测试动作提交;模型根据需要取得同意、委派给真实 Mail + peer,并收集结果。 +4. 保持聊天打开,点击通知检查完整卡片,再点击 × 检查 Glance。触摸位置应以实际 + 屏幕和布局为准;记录的像素坐标只适用于本次手机密度。 +5. 对照 peer 工具结果与宿主日志中的卡片 ID。Agent 报告成功不能单独证明渲染或点击成功。 + +## 发现的问题与验证 + +修复前,聊天遮住通知,且通知只处理鼠标按下,忽略 Android 触摸。修复后, +通知和卡片弹层位于聊天上方,匹配的触摸释放才激活卡片;拖动会取消激活但保持 +事件归属,被其他弹层中断的触摸可恢复。通知打开指定卡片,底层切到 Glance; +卡片 × 和背景支持触摸关闭。 + +在 `c3eaea2c` 上,**838 个 shell 测试通过**,包含 7 个新增触摸回归测试。 +桌面默认和 `mobile-apps` 检查、Home 的 `mobile-apps` 检查、两套 shell 依赖图检查 +以及 `tools/setup.py --check --cargo` 均通过。Android 构建和上述真机流程也通过。 +单元测试不是无窗口 UI 端到端测试;交互证据来自真实手机。 + +## 范围与清理 + +- 验证的是 **OctoSense 应用内通知条**;未验证 Android 系统通知栏、后台通知或内部通知抽屉。 +- `mail.notify` 将内容填入宿主现有 L0 通知模板;没有生成新的 L0 源码,也没有打开 Mail 完整应用。 +- 最终验收未测试 Calendar、真实 IMAP/SMTP、卡片替换、过期或进程重启后的持久化。 +- 较早重放中,Android Back 关闭卡片后也退出了改名后的测试 Activity。当前固定版本的 + Makepad 按 `.MakepadAppExtension` 查找扩展,而 Home 的 Java 类保留原包名, + 因而测试包没有加载 Home 的原生 Back 接管逻辑。最终流程使用 ×;生产 Home 的 Back + 行为在本次测试中仍属**未验证**。 + +上述原始测试结束后,已移除临时供应商配置、恢复屏幕超时时间、停止测试包,并确认 ADB 为非 root。 +保留合成 Mail 状态和测试证据供检查,见[清理记录](evidence/agent-card/cleanup.json)。 + +## 外观调整:组合构建重放 + +**后续竖屏重放通过**,卡片 ID 为 `os.mail/mail-card-polish-1791014993`。 +本次检查 `9e65b3bc` 中的圆角手机通知条、底部卡片弹层、更大的关闭触摸区和 +通知内容间距。实际 APK 来自干净的组合提交 +[`ccf8013f`](https://github.com/OctoSense-org/OctoSense/commit/ccf8013f2bd7adbb6c20d5f52f47bcfbcbb55313),其中还包含 App Studio。 +这**不是仅以 `9e65b3bc` 构建的真机验证**。 +[已安装 APK 指纹](evidence/agent-card-polish/installed-build.json) 与 +[组合构建记录](evidence/agent-card-polish/build.json) 一致;上述原始 +`c3eaea2c` 证据单独保留。 + +[新会话记录](evidence/agent-card-polish/agent-evidence.json) 包含系统 Agent 的 +`peer_send_input`、独立 Mail peer 成功执行的 `mail_notify`(相同卡片 ID, +`replaced: false`),以及 `peer_gather` 和两个会话的最终回复。 +[路由日志](evidence/agent-card-polish/notification-route.log) 与 +[触摸记录](evidence/agent-card-polish/observation.json) 确认:触摸 `(540,270)` +打开该卡片,点击 `(962,1510)` 的 × 后在同一 Activity 内关闭。坐标基于记录的 +450 dpi 屏幕密度。 + +[聊天上方的通知条](evidence/agent-card-polish/01-notification-over-chat.png) · +[完整卡片](evidence/agent-card-polish/02-full-card.png) · +[关闭后的 Glance](evidence/agent-card-polish/03-glance-after-close.png) + +独立截图检查确认文字可读、完整卡片正文齐全、关闭按钮可见、边距完整,关闭后 +Glance 仍显示同一张卡片。通知条正文按设计省略。Glance 状态栏的白色图标与浅色 +背景对比度仍偏低,这是尚存的 shell 问题。通知条后方较早的 Calendar 模型回复 +属于另一项测试。见[视觉检查](evidence/agent-card-polish/visual-review.json) 和 +[文件哈希](evidence/agent-card-polish/sha256.json)。 + +首次外观重放因测试脚本在旧日志快照中寻找尚未记录的触摸矩形而停止。选择触摸 +目标前刷新日志后,脚本恢复正常;运行时代码没有变化。 +[失败尝试摘要](evidence/agent-card-polish/prior-attempt.json) 保留了这一区别。 +成功重放的会话文件也保留了一次较早 Mail 轮次的 `orphaned_by_restart` 错误; +本次验收的发布与最终回复属于另一个轮次。 + +重放复用了现有人工同意记录和合成 demo 账户。流程期间未启用开发者授权;结束后 +将相关文件精确恢复为原字节或原先不存在的状态,并停止测试包。测试包装脚本没有 +改动供应商配置或同意记录,见[清理记录](evidence/agent-card-polish/cleanup.json)。 +生产 Home 未受影响。本次不增加对 Android Back、深色模式、横屏、系统通知栏或 +真实邮件的验证声明。 + +全部模板和通知测试结束后,测试人员另外移除了两个独立测试包的临时供应商配置 +和开发者授权,恢复到原先不存在的状态;停止两个测试包、恢复 60 秒屏幕超时, +并确认 ADB 为非 root。这份后续[最终清理记录](evidence/agent-card-polish/final-cleanup.json) +记录的是包装脚本临时恢复授权之后的最终状态;生产 Home、默认启动器和 ROM +均未改动。 + +组合重放前,`9e65b3bc` 的 **840 个 shell 测试**、桌面默认/mobile-apps 和 +Home mobile-apps 检查、两套依赖图检查、setup 验证与 Android 构建均通过。 +这些检查和组合构建的真机重放对应不同源码范围,不能替代仅针对外观提交构建的 +真机测试。