@@ -14,9 +14,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
14import androidx.compose.foundation.layout.padding
14import androidx.compose.foundation.layout.padding
15import androidx.compose.foundation.layout.size
15import androidx.compose.foundation.layout.size
16import androidx.compose.foundation.lazy.LazyColumn
16import androidx.compose.foundation.lazy.LazyColumn
17+import androidx.compose.foundation.lazy.items
17import androidx.compose.foundation.text.KeyboardActions
18import androidx.compose.foundation.text.KeyboardActions
18import androidx.compose.foundation.text.KeyboardOptions
19import androidx.compose.foundation.text.KeyboardOptions
19−import androidx.compose.foundation.lazy.items
20import androidx.compose.material.icons.Icons
20import androidx.compose.material.icons.Icons
21import androidx.compose.material.icons.automirrored.filled.Logout
21import androidx.compose.material.icons.automirrored.filled.Logout
22import androidx.compose.material.icons.filled.Add
22import androidx.compose.material.icons.filled.Add
@@ -43,10 +43,13 @@ import androidx.compose.material3.OutlinedTextField
43import androidx.compose.material3.Scaffold
43import androidx.compose.material3.Scaffold
44import androidx.compose.material3.SnackbarHost
44import androidx.compose.material3.SnackbarHost
45import androidx.compose.material3.SnackbarHostState
45import androidx.compose.material3.SnackbarHostState
46+import androidx.compose.material3.SwipeToDismissBox
47+import androidx.compose.material3.SwipeToDismissBoxValue
46import androidx.compose.material3.Text
48import androidx.compose.material3.Text
47import androidx.compose.material3.TextButton
49import androidx.compose.material3.TextButton
48import androidx.compose.material3.TopAppBar
50import androidx.compose.material3.TopAppBar
49import androidx.compose.material3.pulltorefresh.PullToRefreshBox
51import androidx.compose.material3.pulltorefresh.PullToRefreshBox
52+import androidx.compose.material3.rememberSwipeToDismissBoxState
50import androidx.compose.runtime.Composable
53import androidx.compose.runtime.Composable
51import androidx.compose.runtime.CompositionLocalProvider
54import androidx.compose.runtime.CompositionLocalProvider
52import androidx.compose.runtime.LaunchedEffect
55import androidx.compose.runtime.LaunchedEffect
@@ -213,11 +216,19 @@ fun SessionListScreen(
213 .semantics { contentDescription = loadingLabel },
216 .semantics { contentDescription = loadingLabel },
216− state.sessions.isEmpty() && state.error != null -> Text(
219+ state.sessions.isEmpty() && state.error != null -> Column(
218 modifier = Modifier.align(Alignment.Center).padding(24.dp),
220 modifier = Modifier.align(Alignment.Center).padding(24.dp),
221+ horizontalAlignment = Alignment.CenterHorizontally,
219− color = MaterialTheme.colorScheme.error,
225+ color = MaterialTheme.colorScheme.error,
227+ Spacer(Modifier.size(12.dp))
228+ TextButton(onClick = { vm.refresh() }) {
229+ Text(stringResource(R.string.retry))
221 state.sessions.isEmpty() -> EmptySessions(
232 state.sessions.isEmpty() -> EmptySessions(
222 onCreate = { vm.createSession(onCreated = onOpenSession) },
233 onCreate = { vm.createSession(onCreated = onOpenSession) },
223 modifier = Modifier.align(Alignment.Center),
234 modifier = Modifier.align(Alignment.Center),
@@ -258,15 +269,46 @@ fun SessionListScreen(
258 verticalArrangement = Arrangement.spacedBy(12.dp),
269 verticalArrangement = Arrangement.spacedBy(12.dp),
260 items(sessions, key = { it.id }) { session ->
271 items(sessions, key = { it.id }) { session ->
261− SessionCard(
272+ // Swipe end-to-start reveals a delete affordance and opens the
262− session = session,
273+ // same confirmation dialog as the trash icon. We never commit
263− preview = state.previews[session.id],
274+ // the dismissal (always reset to Settled) so the card snaps back
264− unread = unread.contains(session.id),
275+ // and the dialog guards against accidental data loss.
265− onClick = { onOpenSession(session.id) },
276+ val swipeState = rememberSwipeToDismissBoxState(
266− onRename = { pendingRenameId = session.id },
277+ confirmValueChange = {
267− onDelete = { pendingDeleteId = session.id },
278+ pendingDeleteId = session.id
268− modifier = Modifier.testTag("session_card"),
279+ false
284+ enableDismissFromStartToEnd = false,
285+ backgroundContent = {
289+ .clip(MaterialTheme.shapes.medium)
290+ .background(MaterialTheme.colorScheme.errorContainer)
291+ .padding(horizontal = 20.dp),
292+ contentAlignment = Alignment.CenterEnd,
295+ Icons.Filled.Delete,
296+ contentDescription = stringResource(R.string.delete),
297+ tint = MaterialTheme.colorScheme.onErrorContainer,
304+ preview = state.previews[session.id],
305+ unread = unread.contains(session.id),
306+ onClick = { onOpenSession(session.id) },
307+ onRename = { pendingRenameId = session.id },
308+ onDelete = { pendingDeleteId = session.id },
309+ modifier = Modifier.testTag("session_card"),