1package soy.iko.opencode.ui.components
3import androidx.compose.foundation.layout.fillMaxHeight
4import androidx.compose.material.icons.Icons
5import androidx.compose.material.icons.automirrored.filled.Chat
6import androidx.compose.material.icons.filled.Dns
7import androidx.compose.material.icons.filled.Folder
8import androidx.compose.material.icons.filled.Hub
9import androidx.compose.material.icons.filled.QueryStats
10import androidx.compose.material.icons.filled.Search
11import androidx.compose.material.icons.filled.Settings
12import androidx.compose.material3.Icon
13import androidx.compose.material3.NavigationRail
14import androidx.compose.material3.NavigationRailItem
15import androidx.compose.material3.NavigationRailItemDefaults
16import androidx.compose.material3.Text
17import androidx.compose.runtime.Composable
18import androidx.compose.ui.Modifier
19import androidx.compose.ui.graphics.vector.ImageVector
20import androidx.compose.ui.res.stringResource
21import soy.iko.opencode.R
23/** A top-level destination surfaced on the large-screen navigation rail. */
24private data class RailDest(
27 val icon: ImageVector,
28 /** Whether the destination needs an active server connection. */
29 val requiresConnection: Boolean,
32private val RAIL_DESTINATIONS = listOf(
33 RailDest("sessions", R.string.sessions_title, Icons.AutoMirrored.Filled.Chat, true),
34 RailDest("search", R.string.search_all_title, Icons.Filled.Search, true),
35 RailDest("files", R.string.files, Icons.Filled.Folder, true),
36 RailDest("usage", R.string.usage_title, Icons.Filled.QueryStats, true),
37 RailDest("mcp", R.string.mcp_servers, Icons.Filled.Hub, true),
38 RailDest("settings", R.string.settings, Icons.Filled.Settings, false),
39 RailDest("servers", R.string.servers_title, Icons.Filled.Dns, false),
43 * A [NavigationRail] shown alongside the nav content on large screens (tablets / foldables), so
44 * the top-level destinations are one tap away instead of buried in a screen's overflow menu.
46 * @param currentRoute the current NavHost destination route, used to highlight the active item.
47 * @param connected whether a server connection is active; connection-dependent items (sessions,
48 * files, …) are disabled until one exists.
49 * @param onNavigate invoked with the destination's route when an item is selected.
52fun LargeScreenNavRail(
53 currentRoute: String?,
55 onNavigate: (String) -> Unit,
56 modifier: Modifier = Modifier,
58 NavigationRail(modifier = modifier.fillMaxHeight()) {
59 RAIL_DESTINATIONS.forEach { dest ->
60 // Match exact top-level routes. Sub-routes (chat/{id}, file_view) don't highlight any
61 // rail item — the rail tracks top-level position, not the detail back stack.
62 val selected = currentRoute == dest.route ||
63 (dest.route == "sessions" && currentRoute != null && currentRoute.startsWith("chat/"))
66 enabled = !dest.requiresConnection || connected,
67 onClick = { onNavigate(dest.route) },
68 icon = { Icon(dest.icon, contentDescription = null) },
69 label = { Text(stringResource(dest.labelRes)) },
70 colors = NavigationRailItemDefaults.colors(),
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