1package soy.iko.opencode.ui.session
3import androidx.compose.foundation.layout.Arrangement
4import androidx.compose.foundation.layout.Box
5import androidx.compose.foundation.layout.Column
6import androidx.compose.foundation.layout.Row
7import androidx.compose.foundation.layout.fillMaxSize
8import androidx.compose.foundation.layout.padding
9import androidx.compose.foundation.layout.widthIn
10import androidx.activity.compose.BackHandler
11import androidx.compose.material.icons.Icons
12import androidx.compose.material.icons.automirrored.filled.Chat
13import androidx.compose.material3.Icon
14import androidx.compose.material3.MaterialTheme
15import androidx.compose.material3.Text
16import androidx.compose.runtime.Composable
17import androidx.compose.runtime.LaunchedEffect
18import androidx.compose.runtime.getValue
19import androidx.compose.runtime.mutableStateOf
20import androidx.compose.runtime.saveable.rememberSaveable
21import androidx.compose.runtime.setValue
22import androidx.compose.ui.Alignment
23import androidx.compose.ui.Modifier
24import androidx.compose.ui.res.stringResource
25import androidx.compose.ui.unit.dp
26import androidx.lifecycle.compose.collectAsStateWithLifecycle
27import soy.iko.opencode.R
28import soy.iko.opencode.data.network.NetworkConfig
29import soy.iko.opencode.di.AppContainer
30import soy.iko.opencode.ui.chat.ChatScreen
33 * Master–detail layout for wide screens (≥ 840dp, e.g. tablets / unfolded foldables):
34 * the session list on the left, the selected conversation on the right. On compact
35 * widths the app uses the single-pane [androidx.navigation] back stack instead.
37 * Each side hosts an existing screen (which carries its own Scaffold + app bar), so the
38 * two panes are self-contained. Selecting a session updates the detail pane rather than
39 * pushing a destination; the chat [onBack] clears the selection. The chat composition is
40 * keyed by session id so each conversation gets its own ViewModel.
43fun TwoPaneSessionChat(
44 container: AppContainer,
45 onOpenFiles: () -> Unit,
46 onOpenSettings: () -> Unit,
47 onDisconnect: () -> Unit,
48 modifier: Modifier = Modifier,
50 var selected by rememberSaveable { mutableStateOf<String?>(null) }
51 val pendingOpenSession by container.pendingOpenSession.collectAsStateWithLifecycle()
52 val pendingShare by container.pendingShare.collectAsStateWithLifecycle()
54 // A notification tap / deep link requests a session: open it in the detail pane.
55 LaunchedEffect(pendingOpenSession) {
56 pendingOpenSession?.let {
58 container.consumePendingOpenSession()
62 // Inject a pending share into the currently selected session's draft (if any).
63 // Unlike single-pane mode where the session list is navigated to specifically for
64 // the share, in two-pane mode the list is always visible — so we inject into the
65 // session the user is already viewing. If no session is selected, the share is
66 // deferred until the user picks one (the LaunchedEffect re-fires when `selected`
67 // changes to non-null).
68 LaunchedEffect(pendingShare, selected) {
70 if (pendingShare != null && target != null) {
71 container.consumePendingShare()?.let { container.draftStore.set(target, it) }
75 Row(modifier = modifier.fillMaxSize()) {
78 .weight(NetworkConfig.twoPaneLeftWeight)
79 .widthIn(max = 460.dp),
82 container = container,
83 onOpenSession = { id ->
86 onDisconnect = onDisconnect,
87 onOpenFiles = onOpenFiles,
88 onOpenSettings = onOpenSettings,
92 Box(modifier = Modifier.weight(NetworkConfig.twoPaneRightWeight).fillMaxSize()) {
93 val sessionId = selected
94 if (sessionId == null) {
97 BackHandler { selected = null }
99 container = container,
100 sessionId = sessionId,
101 onBack = { selected = null },
109private fun EmptyDetail() {
111 modifier = Modifier.fillMaxSize().padding(32.dp),
112 verticalArrangement = Arrangement.Center,
113 horizontalAlignment = Alignment.CenterHorizontally,
116 Icons.AutoMirrored.Filled.Chat,
117 contentDescription = null,
118 tint = MaterialTheme.colorScheme.outline,
121 stringResource(R.string.empty_detail_pane),
122 style = MaterialTheme.typography.bodyLarge,
123 color = MaterialTheme.colorScheme.onSurfaceVariant,
124 modifier = Modifier.padding(top = 12.dp),
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list