@@ -14,9 +14,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
1414import androidx.compose.foundation.layout.padding
1515import androidx.compose.foundation.layout.size
1616import androidx.compose.foundation.lazy.LazyColumn
17+import androidx.compose.foundation.lazy.items
1718import androidx.compose.foundation.text.KeyboardActions
1819import androidx.compose.foundation.text.KeyboardOptions
19−import androidx.compose.foundation.lazy.items
2020import androidx.compose.material.icons.Icons
2121import androidx.compose.material.icons.automirrored.filled.Logout
2222import androidx.compose.material.icons.filled.Add
@@ -43,10 +43,13 @@ import androidx.compose.material3.OutlinedTextField
4343import androidx.compose.material3.Scaffold
4444import androidx.compose.material3.SnackbarHost
4545import androidx.compose.material3.SnackbarHostState
46+import androidx.compose.material3.SwipeToDismissBox
47+import androidx.compose.material3.SwipeToDismissBoxValue
4648import androidx.compose.material3.Text
4749import androidx.compose.material3.TextButton
4850import androidx.compose.material3.TopAppBar
4951import androidx.compose.material3.pulltorefresh.PullToRefreshBox
52+import androidx.compose.material3.rememberSwipeToDismissBoxState
5053import androidx.compose.runtime.Composable
5154import androidx.compose.runtime.CompositionLocalProvider
5255import androidx.compose.runtime.LaunchedEffect
@@ -213,11 +216,19 @@ fun SessionListScreen(
213216 .semantics { contentDescription = loadingLabel },
214217 )
215218 }
216− state.sessions.isEmpty() && state.error != null -> Text(
217− state.error ?: "",
219+ state.sessions.isEmpty() && state.error != null -> Column(
218220 modifier = Modifier.align(Alignment.Center).padding(24.dp),
219− color = MaterialTheme.colorScheme.error,
220− )
221+ horizontalAlignment = Alignment.CenterHorizontally,
222+ ) {
223+ Text(
224+ state.error ?: "",
225+ color = MaterialTheme.colorScheme.error,
226+ )
227+ Spacer(Modifier.size(12.dp))
228+ TextButton(onClick = { vm.refresh() }) {
229+ Text(stringResource(R.string.retry))
230+ }
231+ }
221232 state.sessions.isEmpty() -> EmptySessions(
222233 onCreate = { vm.createSession(onCreated = onOpenSession) },
223234 modifier = Modifier.align(Alignment.Center),
@@ -258,15 +269,46 @@ fun SessionListScreen(
258269 verticalArrangement = Arrangement.spacedBy(12.dp),
259270 ) {
260271 items(sessions, key = { it.id }) { session ->
261− SessionCard(
262− session = session,
263− preview = state.previews[session.id],
264− unread = unread.contains(session.id),
265− onClick = { onOpenSession(session.id) },
266− onRename = { pendingRenameId = session.id },
267− onDelete = { pendingDeleteId = session.id },
268− modifier = Modifier.testTag("session_card"),
272+ // Swipe end-to-start reveals a delete affordance and opens the
273+ // same confirmation dialog as the trash icon. We never commit
274+ // the dismissal (always reset to Settled) so the card snaps back
275+ // and the dialog guards against accidental data loss.
276+ val swipeState = rememberSwipeToDismissBoxState(
277+ confirmValueChange = {
278+ pendingDeleteId = session.id
279+ false
280+ },
269281 )
282+ SwipeToDismissBox(
283+ state = swipeState,
284+ enableDismissFromStartToEnd = false,
285+ backgroundContent = {
286+ Box(
287+ modifier = Modifier
288+ .fillMaxSize()
289+ .clip(MaterialTheme.shapes.medium)
290+ .background(MaterialTheme.colorScheme.errorContainer)
291+ .padding(horizontal = 20.dp),
292+ contentAlignment = Alignment.CenterEnd,
293+ ) {
294+ Icon(
295+ Icons.Filled.Delete,
296+ contentDescription = stringResource(R.string.delete),
297+ tint = MaterialTheme.colorScheme.onErrorContainer,
298+ )
299+ }
300+ },
301+ ) {
302+ SessionCard(
303+ session = session,
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"),
310+ )
311+ }
270312 }
271313 }
272314 }