Leave the session you are reading alone, and put its menus on their buttons
Three things about the session screen. A notification is no longer posted about the session in front of you: the transcript is already saying it, and one that was posted before you opened it is cancelled, since a row in the drawer for the conversation on screen is the same duplication. Bound to RESUMED rather than STARTED, so a session left on this screen behind another app still reports. The model and permission menus opened 142px clear of the buttons that opened them -- the status bar's height, exactly. Compose measures the anchor in window coordinates, which for an edge-to-edge activity is the whole display, but asks whether the menu fits inside the visible frame, which is that less the system bars; sitting just above a control near the bottom then reads as an overflow and Material3 parks the menu near the bottom of the visible frame instead. Turning clipping off makes both questions about the same window. The model picker now offers "default". The button has always been able to say it -- that is what a session with no model of its own reads as -- but the list could not, so choosing any model was a one-way trip. It is the Claude CLI's own word for "whatever is configured", which its set_model accepts, so it is a request rather than a name invented here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
68704fce7c
commit
9d5a7cf602
3 files changed
+86
-7
No files matched your search
@@ -59,6 +59,7 @@ import androidx.lifecycle.repeatOnLifecycle
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -743,6 +744,21 @@ fun SessionScreen(
|
||||
// stays started.
|
||||
DisposableEffect(summary.id) { onDispose { activeStream.get()?.close() } }
|
||||
|
||||
// Nothing gets announced about the session somebody is reading; see NotificationService.
|
||||
// RESUMED rather than STARTED because "looking at it" means the foreground -- a session left
|
||||
// on this screen behind another app is one whose notifications are still wanted, and STARTED
|
||||
// covers that case too.
|
||||
LaunchedEffect(summary.id, lifecycleOwner) {
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
NotificationService.showing(context, summary.id)
|
||||
try {
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
NotificationService.stoppedShowing(summary.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Back at the newest end, so the backlog [apply] held can land. Everything at once rather
|
||||
// than paced out: they are at the bottom, which is the one place the list is allowed to
|
||||
// follow new content, and drip-feeding it would only make that following last longer.
|
||||
@@ -1280,7 +1296,11 @@ fun SessionScreen(
|
||||
if (offeredModels.isNotEmpty()) {
|
||||
PickerButton(
|
||||
current = modelLabel(model),
|
||||
options = offeredModels,
|
||||
// What the machine offers, plus the state a session is in when it
|
||||
// has chosen none of them. The button has always been able to say
|
||||
// "default"; until this the list could not, so leaving it was a
|
||||
// one-way trip.
|
||||
options = listOf(DEFAULT_MODEL) + offeredModels,
|
||||
// Not set here. The button follows what the session reports it
|
||||
// is set to, which arrives a moment later and is sometimes a
|
||||
// different answer -- a name the CLI resolved, or no change at all
|
||||
@@ -1288,7 +1308,7 @@ fun SessionScreen(
|
||||
// Asked about first, unless there is nothing to lose by it --
|
||||
// see [ModelSwitchWarning].
|
||||
onPick = { chosen ->
|
||||
if (chosen == model || items.isEmpty()) {
|
||||
if (modelLabel(chosen) == modelLabel(model) || items.isEmpty()) {
|
||||
act { setSessionModel(settings, summary.id, chosen) }
|
||||
} else {
|
||||
pendingModel = chosen
|
||||
@@ -1593,13 +1613,29 @@ private fun PickerButton(current: String, options: List<String>, onPick: (String
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
// Not focusable, so opening it does not take focus from the message
|
||||
// field and dismiss the keyboard. Changing the model mid-sentence
|
||||
// is an aside, not a departure from what you were typing.
|
||||
// Two departures from the defaults, both deliberate.
|
||||
//
|
||||
// Not focusable, so opening it does not take focus from the message field and dismiss the
|
||||
// keyboard. Changing the model mid-sentence is an aside, not a departure from what you
|
||||
// were typing.
|
||||
//
|
||||
// Not clipped, which is what puts the menu on the button instead of floating above it.
|
||||
// Compose measures the anchor in *window* coordinates -- this app draws edge to edge, so
|
||||
// that window is the whole screen -- but asks whether the menu fits inside the *visible*
|
||||
// frame, which is the screen less the status and navigation bars. Two spaces, one
|
||||
// comparison: sitting just above a button near the bottom then looks like an overflow,
|
||||
// and the menu falls back to a fixed 48dp above the bottom of the visible frame. Measured
|
||||
// on the emulator, that left the menu's foot 142px -- the status bar's height, exactly --
|
||||
// clear of the button that opened it. Turning clipping off makes both questions about the
|
||||
// same window. What it gives up is that the keyboard stops counting as an edge: with the
|
||||
// IME up the menu opens downwards over it rather than upwards over the transcript. That
|
||||
// is the lesser fault -- it is still attached to the button that opened it, which is the
|
||||
// whole complaint -- and correcting it would mean supplying a position provider, which
|
||||
// this menu takes no parameter for.
|
||||
DropdownMenu(
|
||||
expanded = open,
|
||||
onDismissRequest = { open = false },
|
||||
properties = PopupProperties(focusable = false),
|
||||
properties = PopupProperties(focusable = false, clippingEnabled = false),
|
||||
) {
|
||||
options.forEach { option ->
|
||||
DropdownMenuItem(
|
||||
|
||||
Reference in new issue
Block a user