Say it over the app when the app is what somebody is looking at
The notification stream now has three places to land instead of two, decided in one function. Nothing at all for the session on screen, as before. A banner over the app while the app is up. Android's drawer otherwise. Never two of them for one moment: a drawer filling up behind an app that showed you each one is a drawer nobody reads. The banners queue, one per session replacing that session's own -- the rule the drawer already followed, and for the same reason. Each can be tapped, which opens the session by the same path a tapped notification takes; pushed off either side; or left alone, in which case the bar across its foot retires it. The bar and the retiring are one value rather than a bar beside a timer, so a banner cannot outlive the countdown drawn under it. They clear when the app goes away, since a claim that a session wants somebody *now* does not survive an absence -- and the drawer has the job back by then. Which of the three applies needs no flag anybody keeps level. The session on screen is registered by the one composable that draws one, and "the app is up" is the queue being collected, which happens exactly while it is. Also: tapping a model or permission button while its own menu is open now closes it. A non-focusable popup does not swallow the press that dismisses it, so the same finger was reopening what it had just closed -- measured at 3ms between the two, which is what the guard is sized against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e0eaa4b3f8
commit
be47beb0ff
6 files changed
+296
-11
No files matched your search
@@ -1619,6 +1619,16 @@ private fun QuestionRow(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How long after a menu closes a press on its own button still counts as the press that closed it.
|
||||
*
|
||||
* Sized to one tap, because one tap is all it has to span -- [PickerButton] explains the pair of
|
||||
* events it separates. Deliberately not the platform's long-press timeout, which is the longest a
|
||||
* tap can legally be: half a second of ignoring the button would start swallowing a deliberate
|
||||
* reopen, and a press held that long to close a menu is not worth protecting at that price.
|
||||
*/
|
||||
private const val ONE_TAP_MS = 250L
|
||||
|
||||
/** The modes the CLI accepts, in the order they give up asking. */
|
||||
private val PERMISSION_MODES = listOf("manual", "acceptEdits", "auto", "bypassPermissions", "plan")
|
||||
|
||||
@@ -1631,8 +1641,21 @@ private val PERMISSION_MODES = listOf("manual", "acceptEdits", "auto", "bypassPe
|
||||
@Composable
|
||||
private fun PickerButton(current: String, options: List<String>, onPick: (String) -> Unit) {
|
||||
var open by remember { mutableStateOf(false) }
|
||||
// When an outside touch last closed the menu.
|
||||
//
|
||||
// Pressing this button while its own menu is open is such a touch. The menu is deliberately
|
||||
// not focusable (see below), which means the press that dismisses it is also delivered to the
|
||||
// window underneath -- and what it lands on there is this button. The dismissal arrives with
|
||||
// the press and the click with the release, measured 3ms apart on the emulator, so a button
|
||||
// that simply opened on every click would reopen what the same finger had just closed, and
|
||||
// the menu could only be put away by tapping somewhere else. So the moment is remembered, and
|
||||
// a click that follows it within one tap is read as the second half of that tap rather than
|
||||
// as a new one.
|
||||
var closedAt by remember { mutableLongStateOf(0L) }
|
||||
Box {
|
||||
TextButton(onClick = { open = true }) {
|
||||
TextButton(
|
||||
onClick = { if (SystemClock.uptimeMillis() - closedAt > ONE_TAP_MS) open = true }
|
||||
) {
|
||||
// One line, truncated rather than wrapped: this sits in a row
|
||||
// whose height is the buttons beside it, and a second line
|
||||
// would move them.
|
||||
@@ -1664,7 +1687,10 @@ private fun PickerButton(current: String, options: List<String>, onPick: (String
|
||||
// this menu takes no parameter for.
|
||||
DropdownMenu(
|
||||
expanded = open,
|
||||
onDismissRequest = { open = false },
|
||||
onDismissRequest = {
|
||||
open = false
|
||||
closedAt = SystemClock.uptimeMillis()
|
||||
},
|
||||
properties = PopupProperties(focusable = false, clippingEnabled = false),
|
||||
) {
|
||||
options.forEach { option ->
|
||||
|
||||
Reference in new issue
Block a user