package soy.iko.opencode.ui.session import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.activity.compose.BackHandler import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Chat import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import soy.iko.opencode.R import soy.iko.opencode.data.network.NetworkConfig import soy.iko.opencode.di.AppContainer import soy.iko.opencode.ui.chat.ChatScreen /** * Master–detail layout for wide screens (≥ 840dp, e.g. tablets / unfolded foldables): * the session list on the left, the selected conversation on the right. On compact * widths the app uses the single-pane [androidx.navigation] back stack instead. * * Each side hosts an existing screen (which carries its own Scaffold + app bar), so the * two panes are self-contained. Selecting a session updates the detail pane rather than * pushing a destination; the chat [onBack] clears the selection. The chat composition is * keyed by session id so each conversation gets its own ViewModel. */ @Composable fun TwoPaneSessionChat( container: AppContainer, onOpenFiles: () -> Unit, onOpenSettings: () -> Unit, onDisconnect: () -> Unit, modifier: Modifier = Modifier, ) { var selected by rememberSaveable { mutableStateOf(null) } val pendingOpenSession by container.pendingOpenSession.collectAsStateWithLifecycle() val pendingShare by container.pendingShare.collectAsStateWithLifecycle() // A notification tap / deep link requests a session: open it in the detail pane. LaunchedEffect(pendingOpenSession) { pendingOpenSession?.let { selected = it container.consumePendingOpenSession() } } // Inject a pending share into the currently selected session's draft (if any). // Unlike single-pane mode where the session list is navigated to specifically for // the share, in two-pane mode the list is always visible — so we inject into the // session the user is already viewing. If no session is selected, the share is // deferred until the user picks one (the LaunchedEffect re-fires when `selected` // changes to non-null). LaunchedEffect(pendingShare, selected) { val target = selected if (pendingShare != null && target != null) { container.consumePendingShare()?.let { container.draftStore.set(target, it) } } } Row(modifier = modifier.fillMaxSize()) { Box( modifier = Modifier .weight(NetworkConfig.twoPaneLeftWeight) .widthIn(max = 460.dp), ) { SessionListScreen( container = container, onOpenSession = { id -> selected = id }, onDisconnect = onDisconnect, onOpenFiles = onOpenFiles, onOpenSettings = onOpenSettings, ) } Box(modifier = Modifier.weight(NetworkConfig.twoPaneRightWeight).fillMaxSize()) { val sessionId = selected if (sessionId == null) { EmptyDetail() } else { BackHandler { selected = null } ChatScreen( container = container, sessionId = sessionId, onBack = { selected = null }, ) } } } } @Composable private fun EmptyDetail() { Column( modifier = Modifier.fillMaxSize().padding(32.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( Icons.AutoMirrored.Filled.Chat, contentDescription = null, tint = MaterialTheme.colorScheme.outline, ) Text( stringResource(R.string.empty_detail_pane), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 12.dp), ) } }