1package soy.iko.opencode.ui
3import androidx.compose.foundation.layout.BoxWithConstraints
4import androidx.compose.runtime.Composable
5import androidx.compose.runtime.LaunchedEffect
6import androidx.compose.runtime.getValue
7import androidx.compose.ui.unit.dp
8import androidx.navigation.NavType
9import androidx.navigation.compose.NavHost
10import androidx.navigation.compose.composable
11import androidx.navigation.compose.rememberNavController
12import androidx.navigation.navArgument
13import androidx.lifecycle.compose.collectAsStateWithLifecycle
14import soy.iko.opencode.di.AppContainer
15import soy.iko.opencode.ui.chat.ChatScreen
16import soy.iko.opencode.ui.file.FileBrowserScreen
17import soy.iko.opencode.ui.file.FileViewScreen
18import soy.iko.opencode.ui.server.ServerEditScreen
19import soy.iko.opencode.ui.server.ServerListScreen
20import soy.iko.opencode.ui.session.SessionListScreen
21import soy.iko.opencode.ui.session.TwoPaneSessionChat
22import soy.iko.opencode.ui.settings.DiagnosticsScreen
23import soy.iko.opencode.ui.settings.SettingsScreen
26fun OpencodeApp(container: AppContainer) {
27 val navController = rememberNavController()
28 val pendingShare by container.pendingShare.collectAsStateWithLifecycle()
29 val pendingOpenSession by container.pendingOpenSession.collectAsStateWithLifecycle()
30 val connection by container.activeConnection.collectAsStateWithLifecycle()
32 // When text is shared into the app, surface the session list so the user can pick
33 // (or create) a conversation to drop it into. The chosen session's draft is set
34 // in [onOpenSession] below. Fresh launches rely on auto-reconnect to get here.
35 LaunchedEffect(pendingShare) {
36 if (pendingShare == null) return@LaunchedEffect
37 if (container.activeConnection.value == null) return@LaunchedEffect
38 if (!navController.popBackStack(Routes.SESSIONS, inclusive = false)) {
39 navController.navigate(Routes.SESSIONS) { launchSingleTop = true }
43 // Adaptive: on wide screens (tablets / unfolded foldables) show the session list and
44 // the open conversation side by side instead of a single-pane back stack.
46 val isTwoPane = maxWidth >= 840.dp && connection != null
48 // Open a session requested by a notification tap or deep link, once connected.
49 // In two-pane mode the detail pane consumes the request instead of navigating.
50 LaunchedEffect(pendingOpenSession, connection, isTwoPane) {
51 val id = pendingOpenSession ?: return@LaunchedEffect
52 if (connection == null || isTwoPane) return@LaunchedEffect
53 container.consumePendingOpenSession()
54 navController.navigate(Routes.chat(id)) { launchSingleTop = true }
57 NavHost(navController = navController, startDestination = Routes.SERVERS) {
59 composable(Routes.SERVERS) {
61 container = container,
62 onConnected = { navController.navigate(Routes.SESSIONS) },
63 onAddProfile = { navController.navigate(Routes.serverEdit()) },
64 onEditProfile = { id -> navController.navigate(Routes.serverEdit(id)) },
69 route = "${Routes.SERVER_EDIT}?id={id}",
70 arguments = listOf(navArgument("id") { type = NavType.StringType; nullable = true; defaultValue = null }),
73 container = container,
74 profileId = entry.arguments?.getString("id"),
75 onDone = { navController.popBackStack() },
79 composable(Routes.SESSIONS) {
82 container = container,
83 onOpenFiles = { navController.navigate(Routes.FILES) },
84 onOpenSettings = { navController.navigate(Routes.SETTINGS) },
86 container.disconnect()
87 navController.popBackStack(Routes.SERVERS, inclusive = false)
92 container = container,
93 onOpenSession = { id ->
94 container.consumePendingShare()?.let { container.draftStore.set(id, it) }
95 navController.navigate(Routes.chat(id))
98 container.disconnect()
99 navController.popBackStack(Routes.SERVERS, inclusive = false)
101 onOpenFiles = { navController.navigate(Routes.FILES) },
102 onOpenSettings = { navController.navigate(Routes.SETTINGS) },
108 route = "${Routes.CHAT}/{sessionId}",
109 arguments = listOf(navArgument("sessionId") { type = NavType.StringType }),
112 container = container,
113 sessionId = entry.arguments?.getString("sessionId").orEmpty(),
114 onBack = { navController.popBackStack() },
118 composable(Routes.FILES) {
120 container = container,
121 onOpenFile = { path -> navController.navigate(Routes.fileView(path)) },
122 onBack = { navController.popBackStack() },
127 route = "${Routes.FILE_VIEW}?path={path}",
128 arguments = listOf(navArgument("path") { type = NavType.StringType; defaultValue = "" }),
131 container = container,
132 path = entry.arguments?.getString("path").orEmpty(),
133 onBack = { navController.popBackStack() },
137 composable(Routes.SETTINGS) {
139 container = container,
140 onBack = { navController.popBackStack() },
142 navController.popBackStack(Routes.SERVERS, inclusive = false)
144 onOpenDiagnostics = { navController.navigate(Routes.DIAGNOSTICS) },
148 composable(Routes.DIAGNOSTICS) {
149 DiagnosticsScreen(onBack = { navController.popBackStack() })
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