diff --git a/.agents/skills/verify-deepseekbot/features/phone-android-interactions.md b/.agents/skills/verify-deepseekbot/features/phone-android-interactions.md index 63bc987a5f..78dffcce4c 100644 --- a/.agents/skills/verify-deepseekbot/features/phone-android-interactions.md +++ b/.agents/skills/verify-deepseekbot/features/phone-android-interactions.md @@ -4,6 +4,8 @@ The Android development app uses native form sheets for Bot and group profiles, ## Sub-features +- `app-dialogs` opens Chats → Host picker → the designated QA Host's More menu → Unpair. Both menu and confirmation use the rounded app surface with Settings tokens, not the native Android Alert. Cancel, Android Back and tapping outside each close only the prompt and leave the drawer and saved pairing unchanged. Repeat light/dark × font1/1.3/2; all consequence text and actions remain reachable, scrolling when needed. From Chats +, verify the same menu style and the existing creation target; from a prepared QA group, open member removal and disband confirmations without submitting a destructive action on shared data. A separate disposable pairing/group is required to prove destructive success. Async refusal/retry and double-submit isolation have focused logical tests; record whether they were exercised natively. The same adapter preserves existing iOS system presentation; iOS runtime and TalkBack require their own qualification. + - `form-keyboard` requires a real soft keyboard for every editable route: pairing, Inbox custom answers, Activity answer cards, model preset names and search, Host appearance, routine creation/editing, group rename, expanded chat answers/drafts, browser/computer text entry and Chats search. Open a low field and require its native bounds and the following action to stay above the IME. The shared Android script accepts `--mode form-keyboard --title --action ` after opening the designated form. It preserves existing drafts and autofocus modals, refuses floating/hidden keyboards and fails overlap beyond one physical pixel of frame rounding. Repeat focus changes, multiline/caret growth, failed-save draft retention, keyboard dismissal/reopening and font1/1.3/2 in light/dark. Preserve canonical QA state and explicitly queue iOS qualification. - `model-settings` opens Settings → Models and presets for the selected Host's global default and shared preset library, and profile → Model for one Bot's Orchestrator/Assignment overrides. Search the chooser by provider/name, check a no-results query and clear it, save an available model and confirm the canonical Host route. Check Assignment default/allowed choices and reasoning effort, create/edit/apply a designated QA preset, and restore the Bot's original model plan. Read failures retain cached content with retry; failed saves retain the input. Repeat light/dark and system font1/1.3/2. Global writes and preset edits must not rewrite independent Bot plans; changing the Orchestrator must retain Assignment choices/efforts/notes. Restore all QA model settings after the journey. diff --git a/.github/pr-screenshots/2106-android-dialogs/README.md b/.github/pr-screenshots/2106-android-dialogs/README.md new file mode 100644 index 0000000000..248d4ebe6d --- /dev/null +++ b/.github/pr-screenshots/2106-android-dialogs/README.md @@ -0,0 +1,13 @@ +# Android application dialogs — native comparisons + +Six matching theme/font cases for Unpair and Host menu, plus dark/light default-font Create and Disband comparisons: 16 pairs / 32 native screenshots. + +Before ran `c7dc11fddfde5432c597671cce17422fe4283b93`; its dialog implementation matches merged main `ccfcbd17175df2eb019cf5e71b2e038ddd41d14f`. After ran `704fa745bd190d57b491b9b6e56589ffdfbc908b`; app behavior is unchanged at `50d810da197c748fabcb92bf38b6e25a3d5fd0d0` (formatting/test-file isolation only). See `provenance.json` for environment and limits. + +Each image is a fixed matching dialog-focused crop of the real Android frame: x0/y650/width1220/height1500. All dialog panels and actions remain inside the crop. It omits the system status bar and developer chrome; no pixels inside the dialog were edited. Full frames and XML remain private. A development-client Expo Router duplicate-linking warning appeared outside this crop during ADB font-scale changes in both baseline and new runs; the normal cold reopen was separately verified. These images do not establish that warning was fixed. + +`native-matrix.json` records all six native Unpair cases: panel bounds, minimum48dp actions, equal-width normal/intermediate actions, stacked large-text actions, and cancel/Back/backdrop dismissal with the Host drawer and pairing retained. Create cancellation also passed in both themes. `native-extra.json` records group confirmation cancellation, real search-keyboard dismissal on opening a menu, and cold-open retention of the original Host and QA group. No shared Host was unpaired, group disbanded, member removed, message sent or pending request answered. + +Logical tests cover explicit confirmation, single execution while pending, refusal/retry and stale-request isolation. Native destructive submission and native failure injection were not exercised on shared data. iOS native, TalkBack gestures and120FPS remain unqualified. The UI adapter retains iOS system alerts/action sheets and does not replace OS-owned permission prompts. + +`original-speed.mp4` is a real native menu → confirmation → Cancel recording, cropped to the same dialog region and scaled to616px wide, without audio or frame interpolation. It retains227 source frames at their original timing (21.64s). `half-speed.mp4` retains those same227 frames with doubled presentation timestamps (43.13s). The recorder's35s wall-clock limit includes idle time that the variable-frame-rate stream does not encode after its final changed frame. These clips establish interaction presentation, not an FPS benchmark. diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1-create.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1-create.png new file mode 100644 index 0000000000..74ef8f5094 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1-menu.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1-menu.png new file mode 100644 index 0000000000..dd5382aa86 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1-unpair.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1-unpair.png new file mode 100644 index 0000000000..5f3c91a078 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1.3-create.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1.3-create.png new file mode 100644 index 0000000000..6645ee49aa Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1.3-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1.3-menu.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1.3-menu.png new file mode 100644 index 0000000000..6188e40f40 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1.3-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1.3-unpair.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1.3-unpair.png new file mode 100644 index 0000000000..4d3b9eb8cb Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-1.3-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-2-create.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-2-create.png new file mode 100644 index 0000000000..5f69bf5109 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-2-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-2-menu.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-2-menu.png new file mode 100644 index 0000000000..9cf8a04410 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-2-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-2-unpair.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-2-unpair.png new file mode 100644 index 0000000000..6ed3991d67 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-dark-2-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1-create.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1-create.png new file mode 100644 index 0000000000..604f37d619 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1-menu.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1-menu.png new file mode 100644 index 0000000000..b2b674da72 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1-unpair.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1-unpair.png new file mode 100644 index 0000000000..6158817d74 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1.3-create.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1.3-create.png new file mode 100644 index 0000000000..17e09bd9e0 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1.3-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1.3-menu.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1.3-menu.png new file mode 100644 index 0000000000..92bd9e8e17 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1.3-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1.3-unpair.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1.3-unpair.png new file mode 100644 index 0000000000..d31dc6c6a8 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-1.3-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-2-create.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-2-create.png new file mode 100644 index 0000000000..cdb8bbd0b1 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-2-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-2-menu.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-2-menu.png new file mode 100644 index 0000000000..3b5fb032f6 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-2-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-2-unpair.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-2-unpair.png new file mode 100644 index 0000000000..5ac4fcd134 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-after-light-2-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-create-before.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-create-before.png new file mode 100644 index 0000000000..ac01bfd3f5 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-create-before.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/button-menu-before.png b/.github/pr-screenshots/2106-android-dialogs/actions/button-menu-before.png new file mode 100644 index 0000000000..063b19b6eb Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/actions/button-menu-before.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/actions/native-matrix.json b/.github/pr-screenshots/2106-android-dialogs/actions/native-matrix.json new file mode 100644 index 0000000000..0efef56e0f --- /dev/null +++ b/.github/pr-screenshots/2106-android-dialogs/actions/native-matrix.json @@ -0,0 +1,387 @@ +{ + "source": "f960d6e1b6baf61b85a99f273fce85666e48c34d", + "records": [ + { + "mode": "dark", + "font": 1, + "create": { + "panel": [78, 1083, 1142, 1707], + "buttons": [ + { + "label": "新建 PersonaBot", + "bounds": [156, 1291, 1064, 1447] + }, + { + "label": "新建频道", + "bounds": [156, 1473, 1064, 1629] + } + ] + }, + "menu": { + "panel": [78, 1083, 1142, 1707], + "buttons": [ + { + "label": "自定义", + "bounds": [156, 1291, 1064, 1447] + }, + { + "label": "取消配对", + "bounds": [156, 1473, 1064, 1629] + } + ] + }, + "confirmation": { + "panel": [78, 1083, 1142, 1707], + "buttons": [ + { + "label": "取消", + "bounds": [156, 1473, 597, 1629] + }, + { + "label": "取消配对", + "bounds": [623, 1473, 1064, 1629] + } + ] + }, + "cancelRetainsPairing": true + }, + { + "mode": "dark", + "font": 1.3, + "create": { + "panel": [78, 1079, 1142, 1711], + "buttons": [ + { + "label": "新建 PersonaBot", + "bounds": [156, 1295, 1064, 1451] + }, + { + "label": "新建频道", + "bounds": [156, 1477, 1064, 1633] + } + ] + }, + "menu": { + "panel": [78, 1079, 1142, 1711], + "buttons": [ + { + "label": "自定义", + "bounds": [156, 1295, 1064, 1451] + }, + { + "label": "取消配对", + "bounds": [156, 1477, 1064, 1633] + } + ] + }, + "confirmation": { + "panel": [78, 1029, 1142, 1762], + "buttons": [ + { + "label": "取消", + "bounds": [156, 1528, 597, 1684] + }, + { + "label": "取消配对", + "bounds": [623, 1528, 1064, 1684] + } + ] + }, + "cancelRetainsPairing": true + }, + { + "mode": "dark", + "font": 2, + "create": { + "panel": [78, 1057, 1142, 1734], + "buttons": [ + { + "label": "新建 PersonaBot", + "bounds": [156, 1304, 1064, 1467] + }, + { + "label": "新建频道", + "bounds": [156, 1493, 1064, 1656] + } + ] + }, + "menu": { + "panel": [78, 1057, 1142, 1734], + "buttons": [ + { + "label": "自定义", + "bounds": [156, 1304, 1064, 1467] + }, + { + "label": "取消配对", + "bounds": [156, 1493, 1064, 1656] + } + ] + }, + "confirmation": { + "panel": [78, 806, 1142, 1985], + "buttons": [ + { + "label": "取消", + "bounds": [156, 1555, 1064, 1718] + }, + { + "label": "取消配对", + "bounds": [156, 1744, 1064, 1907] + } + ] + }, + "cancelRetainsPairing": true + }, + { + "mode": "light", + "font": 1, + "create": { + "panel": [78, 1083, 1142, 1707], + "buttons": [ + { + "label": "新建 PersonaBot", + "bounds": [156, 1291, 1064, 1447] + }, + { + "label": "新建频道", + "bounds": [156, 1473, 1064, 1629] + } + ] + }, + "menu": { + "panel": [78, 1083, 1142, 1707], + "buttons": [ + { + "label": "自定义", + "bounds": [156, 1291, 1064, 1447] + }, + { + "label": "取消配对", + "bounds": [156, 1473, 1064, 1629] + } + ] + }, + "confirmation": { + "panel": [78, 1083, 1142, 1707], + "buttons": [ + { + "label": "取消", + "bounds": [156, 1473, 597, 1629] + }, + { + "label": "取消配对", + "bounds": [623, 1473, 1064, 1629] + } + ] + }, + "cancelRetainsPairing": true + }, + { + "mode": "light", + "font": 1.3, + "create": { + "panel": [78, 1079, 1142, 1711], + "buttons": [ + { + "label": "新建 PersonaBot", + "bounds": [156, 1295, 1064, 1451] + }, + { + "label": "新建频道", + "bounds": [156, 1477, 1064, 1633] + } + ] + }, + "menu": { + "panel": [78, 1079, 1142, 1711], + "buttons": [ + { + "label": "自定义", + "bounds": [156, 1295, 1064, 1451] + }, + { + "label": "取消配对", + "bounds": [156, 1477, 1064, 1633] + } + ] + }, + "confirmation": { + "panel": [78, 1029, 1142, 1762], + "buttons": [ + { + "label": "取消", + "bounds": [156, 1528, 597, 1684] + }, + { + "label": "取消配对", + "bounds": [623, 1528, 1064, 1684] + } + ] + }, + "cancelRetainsPairing": true + }, + { + "mode": "light", + "font": 2, + "create": { + "panel": [78, 1057, 1142, 1734], + "buttons": [ + { + "label": "新建 PersonaBot", + "bounds": [156, 1304, 1064, 1467] + }, + { + "label": "新建频道", + "bounds": [156, 1493, 1064, 1656] + } + ] + }, + "menu": { + "panel": [78, 1057, 1142, 1734], + "buttons": [ + { + "label": "自定义", + "bounds": [156, 1304, 1064, 1467] + }, + { + "label": "取消配对", + "bounds": [156, 1493, 1064, 1656] + } + ] + }, + "confirmation": { + "panel": [78, 806, 1142, 1985], + "buttons": [ + { + "label": "取消", + "bounds": [156, 1555, 1064, 1718] + }, + { + "label": "取消配对", + "bounds": [156, 1744, 1064, 1907] + } + ] + }, + "cancelRetainsPairing": true + } + ], + "viewport": [1220, 2712], + "crop": [0, 650, 1220, 1500], + "checks": [ + { + "mode": "dark", + "font": 1.0, + "kind": "create", + "noRedundantCancel": true + }, + { + "mode": "dark", + "font": 1.0, + "kind": "menu", + "noRedundantCancel": true + }, + { + "mode": "dark", + "font": 1.0, + "kind": "unpair", + "noRedundantCancel": false + }, + { + "mode": "dark", + "font": 1.3, + "kind": "create", + "noRedundantCancel": true + }, + { + "mode": "dark", + "font": 1.3, + "kind": "menu", + "noRedundantCancel": true + }, + { + "mode": "dark", + "font": 1.3, + "kind": "unpair", + "noRedundantCancel": false + }, + { + "mode": "dark", + "font": 2.0, + "kind": "create", + "noRedundantCancel": true + }, + { + "mode": "dark", + "font": 2.0, + "kind": "menu", + "noRedundantCancel": true + }, + { + "mode": "dark", + "font": 2.0, + "kind": "unpair", + "noRedundantCancel": false + }, + { + "mode": "light", + "font": 1.0, + "kind": "create", + "noRedundantCancel": true + }, + { + "mode": "light", + "font": 1.0, + "kind": "menu", + "noRedundantCancel": true + }, + { + "mode": "light", + "font": 1.0, + "kind": "unpair", + "noRedundantCancel": false + }, + { + "mode": "light", + "font": 1.3, + "kind": "create", + "noRedundantCancel": true + }, + { + "mode": "light", + "font": 1.3, + "kind": "menu", + "noRedundantCancel": true + }, + { + "mode": "light", + "font": 1.3, + "kind": "unpair", + "noRedundantCancel": false + }, + { + "mode": "light", + "font": 2.0, + "kind": "create", + "noRedundantCancel": true + }, + { + "mode": "light", + "font": 2.0, + "kind": "menu", + "noRedundantCancel": true + }, + { + "mode": "light", + "font": 2.0, + "kind": "unpair", + "noRedundantCancel": false + } + ], + "limits": [ + "Android16 SDK58 Debug client", + "iOS and TalkBack unrun", + "no shared unpair/disband submitted", + "earlier captures and videos retain earlier visual identities" + ] +} diff --git a/.github/pr-screenshots/2106-android-dialogs/after-dark-1-menu.png b/.github/pr-screenshots/2106-android-dialogs/after-dark-1-menu.png new file mode 100644 index 0000000000..063b19b6eb Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-dark-1-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-dark-1-unpair.png b/.github/pr-screenshots/2106-android-dialogs/after-dark-1-unpair.png new file mode 100644 index 0000000000..b09c4844b9 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-dark-1-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-dark-1.3-menu.png b/.github/pr-screenshots/2106-android-dialogs/after-dark-1.3-menu.png new file mode 100644 index 0000000000..2b9ffb7c88 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-dark-1.3-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-dark-1.3-unpair.png b/.github/pr-screenshots/2106-android-dialogs/after-dark-1.3-unpair.png new file mode 100644 index 0000000000..773aed6461 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-dark-1.3-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-dark-2-menu.png b/.github/pr-screenshots/2106-android-dialogs/after-dark-2-menu.png new file mode 100644 index 0000000000..515b498310 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-dark-2-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-dark-2-unpair.png b/.github/pr-screenshots/2106-android-dialogs/after-dark-2-unpair.png new file mode 100644 index 0000000000..31b86f1c09 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-dark-2-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-dark-create.png b/.github/pr-screenshots/2106-android-dialogs/after-dark-create.png new file mode 100644 index 0000000000..ac01bfd3f5 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-dark-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-dark-disband.png b/.github/pr-screenshots/2106-android-dialogs/after-dark-disband.png new file mode 100644 index 0000000000..d77c17c6c2 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-dark-disband.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-light-1-menu.png b/.github/pr-screenshots/2106-android-dialogs/after-light-1-menu.png new file mode 100644 index 0000000000..eca46b2463 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-light-1-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-light-1-unpair.png b/.github/pr-screenshots/2106-android-dialogs/after-light-1-unpair.png new file mode 100644 index 0000000000..a34dae1399 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-light-1-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-light-1.3-menu.png b/.github/pr-screenshots/2106-android-dialogs/after-light-1.3-menu.png new file mode 100644 index 0000000000..18e3dd79b6 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-light-1.3-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-light-1.3-unpair.png b/.github/pr-screenshots/2106-android-dialogs/after-light-1.3-unpair.png new file mode 100644 index 0000000000..8b9adca5cc Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-light-1.3-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-light-2-menu.png b/.github/pr-screenshots/2106-android-dialogs/after-light-2-menu.png new file mode 100644 index 0000000000..fdfc2ea25d Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-light-2-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-light-2-unpair.png b/.github/pr-screenshots/2106-android-dialogs/after-light-2-unpair.png new file mode 100644 index 0000000000..6066677850 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-light-2-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-light-create.png b/.github/pr-screenshots/2106-android-dialogs/after-light-create.png new file mode 100644 index 0000000000..d2d7a5d337 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-light-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/after-light-disband.png b/.github/pr-screenshots/2106-android-dialogs/after-light-disband.png new file mode 100644 index 0000000000..0dbb130e7f Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/after-light-disband.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-dark-1-menu.png b/.github/pr-screenshots/2106-android-dialogs/before-dark-1-menu.png new file mode 100644 index 0000000000..3cf4060f51 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-dark-1-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-dark-1-unpair.png b/.github/pr-screenshots/2106-android-dialogs/before-dark-1-unpair.png new file mode 100644 index 0000000000..6abe4fb7e4 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-dark-1-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-dark-1.3-menu.png b/.github/pr-screenshots/2106-android-dialogs/before-dark-1.3-menu.png new file mode 100644 index 0000000000..2ff02314d6 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-dark-1.3-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-dark-1.3-unpair.png b/.github/pr-screenshots/2106-android-dialogs/before-dark-1.3-unpair.png new file mode 100644 index 0000000000..ff4f1bf48c Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-dark-1.3-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-dark-2-menu.png b/.github/pr-screenshots/2106-android-dialogs/before-dark-2-menu.png new file mode 100644 index 0000000000..8d2d21d934 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-dark-2-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-dark-2-unpair.png b/.github/pr-screenshots/2106-android-dialogs/before-dark-2-unpair.png new file mode 100644 index 0000000000..4ef2e8f026 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-dark-2-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-dark-create.png b/.github/pr-screenshots/2106-android-dialogs/before-dark-create.png new file mode 100644 index 0000000000..b9d36734fa Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-dark-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-dark-disband.png b/.github/pr-screenshots/2106-android-dialogs/before-dark-disband.png new file mode 100644 index 0000000000..dbcf9fe0be Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-dark-disband.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-light-1-menu.png b/.github/pr-screenshots/2106-android-dialogs/before-light-1-menu.png new file mode 100644 index 0000000000..b730be0677 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-light-1-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-light-1-unpair.png b/.github/pr-screenshots/2106-android-dialogs/before-light-1-unpair.png new file mode 100644 index 0000000000..a18810b8ce Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-light-1-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-light-1.3-menu.png b/.github/pr-screenshots/2106-android-dialogs/before-light-1.3-menu.png new file mode 100644 index 0000000000..d50dc95cdd Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-light-1.3-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-light-1.3-unpair.png b/.github/pr-screenshots/2106-android-dialogs/before-light-1.3-unpair.png new file mode 100644 index 0000000000..6cb6e40763 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-light-1.3-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-light-2-menu.png b/.github/pr-screenshots/2106-android-dialogs/before-light-2-menu.png new file mode 100644 index 0000000000..1401d632b2 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-light-2-menu.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-light-2-unpair.png b/.github/pr-screenshots/2106-android-dialogs/before-light-2-unpair.png new file mode 100644 index 0000000000..c0b941e829 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-light-2-unpair.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-light-create.png b/.github/pr-screenshots/2106-android-dialogs/before-light-create.png new file mode 100644 index 0000000000..ce7f17bc67 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-light-create.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/before-light-disband.png b/.github/pr-screenshots/2106-android-dialogs/before-light-disband.png new file mode 100644 index 0000000000..86087448da Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/before-light-disband.png differ diff --git a/.github/pr-screenshots/2106-android-dialogs/half-speed.mp4 b/.github/pr-screenshots/2106-android-dialogs/half-speed.mp4 new file mode 100644 index 0000000000..5e4a91ff29 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/half-speed.mp4 differ diff --git a/.github/pr-screenshots/2106-android-dialogs/native-extra.json b/.github/pr-screenshots/2106-android-dialogs/native-extra.json new file mode 100644 index 0000000000..430c2c4736 --- /dev/null +++ b/.github/pr-screenshots/2106-android-dialogs/native-extra.json @@ -0,0 +1,22 @@ +[ + { + "mode": "dark", + "groupConfirmation": true, + "cancelRetainsGroup": true + }, + { + "mode": "light", + "groupConfirmation": true, + "cancelRetainsGroup": true + }, + { + "keyboardBeforeVisible": true, + "keyboardAfterVisible": false, + "menuVisible": true + }, + { + "coldOpenPairingRetained": true, + "qaGroupRetained": true, + "noDestructiveSubmit": true + } +] diff --git a/.github/pr-screenshots/2106-android-dialogs/native-matrix.json b/.github/pr-screenshots/2106-android-dialogs/native-matrix.json new file mode 100644 index 0000000000..ea206e89ab --- /dev/null +++ b/.github/pr-screenshots/2106-android-dialogs/native-matrix.json @@ -0,0 +1,81 @@ +{ + "source": "704fa745bd190d57b491b9b6e56589ffdfbc908b", + "records": [ + { + "mode": "dark", + "font": 1, + "dismiss": "cancel", + "panel": [78, 1083, 1142, 1707], + "buttons": [ + [156, 1473, 597, 1629], + [623, 1473, 1064, 1629] + ], + "passed": true + }, + { + "mode": "dark", + "font": 1.3, + "dismiss": "back", + "panel": [78, 1029, 1142, 1762], + "buttons": [ + [156, 1528, 597, 1684], + [623, 1528, 1064, 1684] + ], + "passed": true + }, + { + "mode": "dark", + "font": 2, + "dismiss": "backdrop", + "panel": [78, 806, 1142, 1985], + "buttons": [ + [156, 1555, 1064, 1718], + [156, 1744, 1064, 1907] + ], + "passed": true + }, + { + "mode": "light", + "font": 1, + "dismiss": "cancel", + "panel": [78, 1083, 1142, 1707], + "buttons": [ + [156, 1473, 597, 1629], + [623, 1473, 1064, 1629] + ], + "passed": true + }, + { + "mode": "light", + "font": 1.3, + "dismiss": "back", + "panel": [78, 1029, 1142, 1762], + "buttons": [ + [156, 1528, 597, 1684], + [623, 1528, 1064, 1684] + ], + "passed": true + }, + { + "mode": "light", + "font": 2, + "dismiss": "backdrop", + "panel": [78, 806, 1142, 1985], + "buttons": [ + [156, 1555, 1064, 1718], + [156, 1744, 1064, 1907] + ], + "passed": true + }, + { + "mode": "dark", + "createCancel": true, + "passed": true + }, + { + "mode": "light", + "createCancel": true, + "passed": true + } + ] +} diff --git a/.github/pr-screenshots/2106-android-dialogs/original-speed.mp4 b/.github/pr-screenshots/2106-android-dialogs/original-speed.mp4 new file mode 100644 index 0000000000..be73416137 Binary files /dev/null and b/.github/pr-screenshots/2106-android-dialogs/original-speed.mp4 differ diff --git a/.github/pr-screenshots/2106-android-dialogs/provenance.json b/.github/pr-screenshots/2106-android-dialogs/provenance.json new file mode 100644 index 0000000000..47fc498980 --- /dev/null +++ b/.github/pr-screenshots/2106-android-dialogs/provenance.json @@ -0,0 +1,136 @@ +{ + "beforeSource": "c7dc11fddfde5432c597671cce17422fe4283b93", + "base": "ccfcbd17175df2eb019cf5e71b2e038ddd41d14f", + "beforeDialogCodeMatchesBase": true, + "afterRuntime": "704fa745bd190d57b491b9b6e56589ffdfbc908b", + "afterCodeUnchangedAt": "50d810da197c748fabcb92bf38b6e25a3d5fd0d0", + "android": 16, + "client": "Expo SDK58 development client; JS-only change", + "viewport": { + "width": 1220, + "height": 2712, + "density": 3.25 + }, + "crop": { + "top": 650, + "height": 1500, + "reason": "Fixed matching dialog-focused crop; includes every panel in full and omits status bar and developer chrome. Full frames retained privately." + }, + "groups": [ + { + "kind": "unpair", + "mode": "dark", + "font": 1, + "stem": "dark-1-unpair" + }, + { + "kind": "unpair", + "mode": "dark", + "font": 1.3, + "stem": "dark-1.3-unpair" + }, + { + "kind": "unpair", + "mode": "dark", + "font": 2, + "stem": "dark-2-unpair" + }, + { + "kind": "unpair", + "mode": "light", + "font": 1, + "stem": "light-1-unpair" + }, + { + "kind": "unpair", + "mode": "light", + "font": 1.3, + "stem": "light-1.3-unpair" + }, + { + "kind": "unpair", + "mode": "light", + "font": 2, + "stem": "light-2-unpair" + }, + { + "kind": "menu", + "mode": "dark", + "font": 1, + "stem": "dark-1-menu" + }, + { + "kind": "menu", + "mode": "dark", + "font": 1.3, + "stem": "dark-1.3-menu" + }, + { + "kind": "menu", + "mode": "dark", + "font": 2, + "stem": "dark-2-menu" + }, + { + "kind": "menu", + "mode": "light", + "font": 1, + "stem": "light-1-menu" + }, + { + "kind": "menu", + "mode": "light", + "font": 1.3, + "stem": "light-1.3-menu" + }, + { + "kind": "menu", + "mode": "light", + "font": 2, + "stem": "light-2-menu" + }, + { + "kind": "create", + "mode": "dark", + "font": 1, + "stem": "dark-create" + }, + { + "kind": "create", + "mode": "light", + "font": 1, + "stem": "light-create" + }, + { + "kind": "disband", + "mode": "dark", + "font": 1, + "stem": "dark-disband" + }, + { + "kind": "disband", + "mode": "light", + "font": 1, + "stem": "light-disband" + } + ], + "qualified": [ + "native theme/font matrix for Host menu and unpair confirmation", + "Cancel, Back and backdrop preserve drawer/pairing", + "create menu cancellation", + "group disband confirmation cancellation", + "real search IME dismissed when menu opens", + "cold reopen retains original Host and QA group" + ], + "notQualified": [ + "iOS native", + "TalkBack gestures", + "native destructive submission on shared QA data", + "native failure/retry injection", + "120FPS" + ], + "observations": [ + "Expo Router duplicate-linking warnings occurred during ADB font-scale configuration changes in the development client; bounds/interactions passed and normal cold reopen was checked separately." + ], + "publicSafety": "No raw XML, pairing offers, credentials, user reference-app attachments or unrelated app captures published." +} diff --git a/apps/mobile/DESIGN.md b/apps/mobile/DESIGN.md index 8b2e226406..e830cabebb 100644 --- a/apps/mobile/DESIGN.md +++ b/apps/mobile/DESIGN.md @@ -11,6 +11,14 @@ References: | Meta Muse | A Bot profile whose identity and memory are visible and editable. | | Slack | Workspace switching, a row of shortcut cards on Chats, an Activity feed with filters, and agent steps with a Stop button. | +## Android application dialogs + +- Application-owned menus and confirmations use `showAppDialog` and the root `AppDialogHost`, matching the accepted OCAT-inspired Settings surfaces. Do not call React Native `Alert.alert` directly from a screen or feature. iOS retains its existing system alerts/action sheets; operating-system permission prompts, photo pickers and the share sheet remain system-owned. +- Use `theme.overlay.fill`, the same `androidSettings.radius` as Settings groups, the shared scrim, semantic text and existing Button/SettingsRow primitives. Keep the panel centered with safe-area gutters and a bounded width; do not stretch a short confirmation to fill the screen or add heavy shadows. Contents scroll at large system text sizes; confirmation actions stack when horizontal space is limited. Never cap font scaling or truncate the consequence text. +- Choice menus show separate filled tonal buttons for same-priority actions, with icons, visible containers and pressed/ripple feedback. Use the shared accent container and readable on-container label; do not leave key actions as unbounded text. Omit the redundant Cancel button in these menus: backdrop, Back and accessibility escape dismiss without choosing. Use colors rather than button outlines. Destructive menu choices use the shared danger fill and open a second confirmation describing the consequence, with a low-emphasis explicit Cancel and a destructive action. Keep the current drawer/page underneath. This adapts the [Android button hierarchy](https://developer.android.com/develop/ui/compose/quick-guides/content/create-button) to our OCAT-inspired app surfaces; a menu's equivalent actions are not all primary final confirmations. +- While an asynchronous action is pending, disable repeat submission and dismissal; close after success. On failure, keep the prompt and retry available. A menu opening a confirmation must not dismiss the newer prompt when its old callback finishes. Dialog requests are transient UI state, never a second persistence or authorization owner. +- Native checks include cancel/button/Back/backdrop paths, nested confirmation, light/dark and default/intermediate/maximum text. Verify the original pairing/group remains unchanged after cancellation, keyboard dismissal from focused search, and the actual rendering above an open drawer or profile. Record iOS and TalkBack qualification separately. + ## Activity query controls - Activity has two header entries: **Filter and sort** and **Search Activity**. Sorting is part of the same options sheet as category, unread-only, Bot and conversation filters. diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index c74d0d4adf..6ca3d26090 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -7,6 +7,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context'; import { usePushRouting } from '../src/host/push-routing'; import { StatisticsNotice } from '../src/ui/shell/statistics-notice'; import { KeyboardRoot } from '../src/ui/primitives/keyboard-frame'; +import { AppDialogHost } from '../src/ui/primitives/app-dialog-host'; import { sheetScreenOptions } from '../src/ui/primitives/sheet-screen'; import { useDesignTheme } from '../src/ui/design/theme'; import { t } from '../src/ui/strings'; @@ -109,6 +110,7 @@ export default function RootLayout() { /> + diff --git a/apps/mobile/src/ui/chat/group-info.tsx b/apps/mobile/src/ui/chat/group-info.tsx index 319d803c88..0ae1569529 100644 --- a/apps/mobile/src/ui/chat/group-info.tsx +++ b/apps/mobile/src/ui/chat/group-info.tsx @@ -1,6 +1,7 @@ import { TextInput } from '../primitives/system-text'; import { useState, type ReactNode } from 'react'; -import { Alert, FlatList, Pressable, SectionList, View } from 'react-native'; +import { FlatList, Pressable, SectionList, View } from 'react-native'; +import { showAppDialog } from '../primitives/app-dialog'; import { KeyboardFormScroll } from '../primitives/keyboard-form'; import { errorText } from '../../host/bot-settings'; @@ -292,19 +293,18 @@ export function GroupInfoSheet({ const memberMenu = (member: GroupMember) => { const name = memberName(member); - Alert.alert(name, undefined, [ + showAppDialog(name, undefined, [ { text: t.groupOpenProfile, onPress: () => onOpenBot(member.slug) }, { text: t.groupRemove, style: 'destructive', onPress: () => - Alert.alert(t.groupRemoveConfirm(name), undefined, [ + showAppDialog(t.groupRemoveConfirm(name), undefined, [ { text: t.cancel, style: 'cancel' }, { text: t.groupRemove, style: 'destructive', - onPress: () => - void run(`member:${member.slug}`, () => actions.removeMember(member.slug)), + onPress: () => run(`member:${member.slug}`, () => actions.removeMember(member.slug)), }, ]), }, @@ -313,14 +313,15 @@ export function GroupInfoSheet({ }; const confirmDisband = () => - Alert.alert(t.groupDisbandConfirm(channel.name), t.groupDisbandDetail, [ + showAppDialog(t.groupDisbandConfirm(channel.name), t.groupDisbandDetail, [ { text: t.cancel, style: 'cancel' }, { text: t.groupDisband, style: 'destructive', onPress: () => - void run('disband', actions.disband).then((ok) => { + run('disband', actions.disband).then((ok) => { if (ok) onDisbanded(); + return ok; }), }, ]); diff --git a/apps/mobile/src/ui/design/controls.ts b/apps/mobile/src/ui/design/controls.ts index f14836b554..a071da6662 100644 --- a/apps/mobile/src/ui/design/controls.ts +++ b/apps/mobile/src/ui/design/controls.ts @@ -1,7 +1,7 @@ import { contrast, type DesignTheme } from './resolve-theme'; import { distance, over, SEPARATION, tint, type LayerName } from './layers'; -export const BUTTON_TONES = ['primary', 'secondary', 'danger'] as const; +export const BUTTON_TONES = ['primary', 'secondary', 'tonal', 'danger'] as const; export type ButtonTone = (typeof BUTTON_TONES)[number]; export type ButtonColors = { @@ -40,6 +40,25 @@ export function buttonColors(theme: DesignTheme, tone: ButtonTone, on: LayerName label: readable(theme.color.base, fill, behind.fill), }; } + if (tone === 'tonal') { + const alternatives = [ + behind.fill, + behind.danger, + buttonColors(theme, 'secondary', on).fill, + theme.color.fg, + ]; + const fill = + Array.from({ length: 21 }, (_, step) => + tint(behind.chosen, theme.color.accent, step / 20), + ).find((candidate) => + alternatives.every((color) => distance(candidate, color) >= SEPARATION), + ) ?? behind.chosen; + return { + fill, + pressed: tint(fill, theme.color.fg, 0.16), + label: readable(theme.color.accent, fill, theme.color.fg), + }; + } if (tone === 'secondary') { const next = theme.layer[NEXT[on]]; const fill = diff --git a/apps/mobile/src/ui/primitives/app-dialog-host.tsx b/apps/mobile/src/ui/primitives/app-dialog-host.tsx new file mode 100644 index 0000000000..62b04350f7 --- /dev/null +++ b/apps/mobile/src/ui/primitives/app-dialog-host.tsx @@ -0,0 +1,184 @@ +import { useRef, useSyncExternalStore } from 'react'; +import { + AccessibilityInfo, + Modal, + Platform, + Pressable, + ScrollView, + View, + type ViewInstance, +} from 'react-native'; +import { useReducedMotion } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +import { useDesignTheme } from '../design/theme'; +import { buttonColors } from '../design/controls'; +import { androidSettings, modalRail, space } from '../design/tokens'; +import { t } from '../strings'; +import { Button } from './button'; +import { appDialogs } from './dialog-state'; +import { Text } from './text'; +import { ChatsIcon, CloseIcon, ComputerIcon, PersonIcon, SettingsIcon } from '../shell/icons'; + +const ACTION_ICONS = { + person: PersonIcon, + chat: ChatsIcon, + settings: SettingsIcon, + close: CloseIcon, + computer: ComputerIcon, +}; + +export function AppDialogHost() { + const shown = useSyncExternalStore( + appDialogs.subscribe, + appDialogs.getSnapshot, + appDialogs.getSnapshot, + ); + const theme = useDesignTheme(); + const insets = useSafeAreaInsets(); + const reducedMotion = useReducedMotion(); + const heading = useRef(null); + if (Platform.OS !== 'android' || shown === undefined) return null; + const choices = shown.actions.flatMap((action, index) => + action.style === 'cancel' ? [] : [{ action, index }], + ); + const cancel = shown.actions.find((action) => action.style === 'cancel'); + const menu = choices.length > 1; + const stacked = theme.largeText || menu; + const actionStyle = stacked ? undefined : { width: '50%' as const, flexShrink: 1, minWidth: 0 }; + const dismiss = () => appDialogs.dismiss(); + return ( + { + if (heading.current) AccessibilityInfo.sendAccessibilityEvent?.(heading.current, 'focus'); + }} + > + + + + + + + {shown.title} + + + {shown.message === undefined ? null : {shown.message}} + {shown.error === undefined ? null : ( + + {t.dialogFailed} {shown.error} + + )} + {menu ? ( + + {choices.map(({ action, index }) => { + const Icon = action.icon === undefined ? undefined : ACTION_ICONS[action.icon]; + const color = buttonColors( + theme, + action.style === 'destructive' ? 'danger' : 'tonal', + 'sheet', + ).label; + return ( +