1package soy.iko.opencode.ui.file
3import androidx.activity.compose.BackHandler
4import androidx.compose.foundation.layout.Box
5import androidx.compose.foundation.layout.Row
6import androidx.compose.foundation.layout.fillMaxHeight
7import androidx.compose.foundation.layout.fillMaxSize
8import androidx.compose.foundation.layout.widthIn
9import androidx.compose.material.icons.Icons
10import androidx.compose.material.icons.filled.Description
11import androidx.compose.material3.VerticalDivider
12import androidx.compose.runtime.Composable
13import androidx.compose.runtime.LaunchedEffect
14import androidx.compose.runtime.getValue
15import androidx.compose.runtime.key
16import androidx.compose.runtime.mutableStateOf
17import androidx.compose.runtime.remember
18import androidx.compose.runtime.saveable.rememberSaveable
19import androidx.compose.runtime.setValue
20import androidx.compose.ui.Modifier
21import androidx.compose.ui.res.stringResource
22import androidx.compose.ui.unit.dp
23import androidx.lifecycle.compose.collectAsStateWithLifecycle
24import soy.iko.opencode.R
25import soy.iko.opencode.data.network.NetworkConfig
26import soy.iko.opencode.di.AppContainer
27import soy.iko.opencode.ui.components.EmptyState
30 * Master–detail layout for the file browser on wide screens (≥ 840dp): the directory listing
31 * on the left, the selected file's viewer on the right. Mirrors [TwoPaneSessionChat]: each
32 * side hosts an existing screen (which carries its own Scaffold + app bar), so the panes are
33 * self-contained. Selecting a file updates the detail pane rather than pushing a destination;
34 * the viewer's onBack clears the selection.
37fun TwoPaneFileBrowser(
38 container: AppContainer,
40 modifier: Modifier = Modifier,
42 var selected by rememberSaveable { mutableStateOf<String?>(null) }
43 // A deep-linked file open (opencode://file/{path}) targets the detail pane here.
44 val pendingOpenFile by container.pendingOpenFile.collectAsStateWithLifecycle()
45 LaunchedEffect(pendingOpenFile) {
46 pendingOpenFile?.let {
48 container.consumePendingOpenFile()
52 // Stale-selection cleanup: if the selected file is deleted server-side (or otherwise
53 // disappears from the listing), clear the selection so the detail pane doesn't keep
54 // showing stale content. Mirrors TwoPaneSessionChat's equivalent for sessions. We only
55 // clear when the listing is loaded and non-empty (so a transient empty load doesn't wipe
56 // the selection); the FileViewScreen's own error state handles a genuinely-missing file.
57 val connection by container.activeConnection.collectAsStateWithLifecycle()
58 LaunchedEffect(connection, selected) {
59 val conn = connection ?: return@LaunchedEffect
60 val path = selected ?: return@LaunchedEffect
61 // The file browser's listing is cached in the VM; check the current directory's
62 // entries via the repository's file-listing API for the selected file's parent.
63 // This is a lightweight check that runs once per selection change.
64 val parent = path.substringBeforeLast('/', missingDelimiterValue = "")
65 if (parent.isBlank()) return@LaunchedEffect
66 val listing = runCatching { conn.api.listDirectory(parent) }.getOrNull() ?: return@LaunchedEffect
67 val stillExists = listing.any { it.path == path }
68 if (!stillExists) selected = null
71 Row(modifier = modifier.fillMaxSize()) {
74 .weight(NetworkConfig.twoPaneLeftWeight)
75 .widthIn(max = 460.dp),
78 container = container,
79 // Selecting a file opens it in the detail pane (not the back stack).
80 onOpenFile = { path, _ -> selected = path },
85 VerticalDivider(modifier = Modifier.fillMaxHeight())
87 Box(modifier = Modifier.weight(NetworkConfig.twoPaneRightWeight).fillMaxSize()) {
91 icon = Icons.Filled.Description,
92 title = stringResource(R.string.empty_file_detail_title),
93 description = stringResource(R.string.empty_file_detail_desc),
94 modifier = Modifier.fillMaxSize(),
97 BackHandler { selected = null }
98 // key() on the path so switching files in two-pane mode gives FileViewScreen a
99 // fresh composition (its find/wrap state and scroll are per-file, like the chat
100 // pane's per-session keying).
103 container = container,
105 onBack = { selected = null },
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