Give the transcript a way back, and stop losing an import's name
**Every imported session was called "claude-cli session".** The app has nothing to say about the title -- the server names it after the session it is continuing -- so it sent `""`. That is `Some`, which satisfied the `or_else` meant to catch "no title given", so the imported name was computed and then thrown away in favour of the `<provider> session` fallback. Normalised at the boundary instead: blank means absent, because that is what it means to the person who left it blank. Both the client's value and the imported one go through the same trim, so neither can be a string of spaces standing in for a name. **And a jump-to-latest button**, shown only while the newest message is off-screen. Reading back through a conversation is a place to be rather than a state to be rescued from, so it waits to be wanted and leaves once there is nowhere to jump to. It says where it goes instead of drawing an arrow. The list is laid out from the bottom, so "down" in the data is up on the screen, and an arrow would be asking the reader to hold that in their head to press a button. Verified on screen: an import now arrives titled "ai-app" rather than "claude-cli session", and the button appears on scrolling back, returns to the newest message, and disappears on arrival.
This commit is contained in:
1 parent
dcb158ee44
commit
2fe34176c0
3 files changed
+35
-3
No files matched your search
@@ -165,6 +165,10 @@ fun ImportScreen(
|
||||
settings,
|
||||
setup = setup.id,
|
||||
provider = useProvider.name,
|
||||
// Nothing to say: the
|
||||
// server titles it from
|
||||
// the session it is
|
||||
// continuing.
|
||||
title = "",
|
||||
permissionMode = permissionMode,
|
||||
import = session.id,
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -405,10 +406,11 @@ fun SessionScreen(
|
||||
// it: the first frame is already the newest message, and older
|
||||
// ones are composed only as somebody scrolls back to them, which
|
||||
// is also what makes history cheap on a long conversation.
|
||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
reverseLayout = true,
|
||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
@@ -450,6 +452,22 @@ fun SessionScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Only while the newest message is off-screen. Reading back
|
||||
// through a conversation is a place to be, not a state to be
|
||||
// rescued from, so this waits to be wanted -- and says where it
|
||||
// goes rather than drawing an arrow, since an arrow in a list
|
||||
// that is laid out upside down is the one thing nobody should
|
||||
// have to reason about.
|
||||
if (!followTail) {
|
||||
FilledTonalButton(
|
||||
onClick = { scope.launch { listState.animateScrollToItem(0) } },
|
||||
modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 12.dp),
|
||||
) {
|
||||
Text("Jump to latest", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always enabled -- a send while the session is running becomes a
|
||||
// steering message injected at the next tool boundary, which is
|
||||
// the point of the whole app.
|
||||
|
||||
+12
-2
@@ -489,10 +489,20 @@ async fn spawn_session(
|
||||
provider: body.provider,
|
||||
// An imported session is recognised by what it was about, so its
|
||||
// opening message is the title unless one was typed.
|
||||
title: body.title.or_else(|| {
|
||||
// Blank normalised to absent here rather than trusted as a
|
||||
// choice. A client with nothing to say sends `""`, which is
|
||||
// `Some` and so satisfied `or_else` -- the imported session's real
|
||||
// title was computed, then discarded in favour of the "<provider>
|
||||
// session" fallback, so every import arrived called "claude-cli
|
||||
// session". Absent and empty mean the same thing to a person and
|
||||
// have to mean the same thing here.
|
||||
title: body
|
||||
.title
|
||||
.filter(|title| !title.trim().is_empty())
|
||||
.or_else(|| {
|
||||
seed.as_ref()
|
||||
.map(|(chosen, _)| chosen.title.clone())
|
||||
.filter(|title| !title.is_empty())
|
||||
.filter(|title| !title.trim().is_empty())
|
||||
}),
|
||||
model: body.model,
|
||||
// Resumed where it was working, so the CLI picks up the same tree.
|
||||
|
||||
Reference in new issue
Block a user