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.runtime.rememberCoroutineScope
8import androidx.compose.ui.unit.dp
9import androidx.navigation.NavType
10import androidx.navigation.compose.NavHost
11import androidx.navigation.compose.composable
12import androidx.navigation.compose.rememberNavController
13import androidx.navigation.navArgument
14import androidx.lifecycle.compose.collectAsStateWithLifecycle
15import kotlinx.coroutines.launch
16import soy.iko.opencode.di.AppContainer
17import soy.iko.opencode.ui.chat.ChatScreen
18import soy.iko.opencode.ui.file.FileBrowserScreen
19import soy.iko.opencode.ui.file.FileViewScreen
20import soy.iko.opencode.ui.server.ServerEditScreen
21import soy.iko.opencode.ui.server.ServerListScreen
22import soy.iko.opencode.ui.session.SessionListScreen
23import soy.iko.opencode.ui.session.TwoPaneSessionChat
24import soy.iko.opencode.ui.settings.DiagnosticsScreen
25import soy.iko.opencode.ui.settings.SettingsScreen
28fun OpencodeApp(container: AppContainer) {
29 val navController = rememberNavController()
30 val scope = rememberCoroutineScope()
31 val pendingShare by container.pendingShare.collectAsStateWithLifecycle()
32 val pendingOpenSession by container.pendingOpenSession.collectAsStateWithLifecycle()
33 val connection by container.activeConnection.collectAsStateWithLifecycle()
35 // When text is shared into the app, surface the session list so the user can pick
36 // (or create) a conversation to drop it into. The chosen session's draft is set
37 // in [onOpenSession] below. Fresh launches rely on auto-reconnect to get here.
38 LaunchedEffect(pendingShare) {
39 if (pendingShare == null) return@LaunchedEffect
40 if (container.activeConnection.value == null) return@LaunchedEffect
41 if (!navController.popBackStack(Routes.SESSIONS, inclusive = false)) {
42 navController.navigate(Routes.SESSIONS) { launchSingleTop = true }
46 // Adaptive: on wide screens (tablets / unfolded foldables) show the session list and
47 // the open conversation side by side instead of a single-pane back stack.
49 val isTwoPane = maxWidth >= 840.dp && connection != null
51 // Open a session requested by a notification tap or deep link, once connected.
52 // In two-pane mode the detail pane consumes the request instead of navigating.
53 LaunchedEffect(pendingOpenSession, connection, isTwoPane) {
54 val id = pendingOpenSession ?: return@LaunchedEffect
55 if (connection == null || isTwoPane) return@LaunchedEffect
56 container.consumePendingOpenSession()
57 navController.navigate(Routes.chat(id)) { launchSingleTop = true }
60 NavHost(navController = navController, startDestination = Routes.SERVERS) {
62 composable(Routes.SERVERS) {
64 container = container,
65 onConnected = { navController.navigate(Routes.SESSIONS) },
66 onAddProfile = { navController.navigate(Routes.serverEdit()) },
67 onEditProfile = { id -> navController.navigate(Routes.serverEdit(id)) },
72 route = "${Routes.SERVER_EDIT}?id={id}",
73 arguments = listOf(navArgument("id") { type = NavType.StringType; nullable = true; defaultValue = null }),
76 container = container,
77 profileId = entry.arguments?.getString("id"),
78 onDone = { navController.popBackStack() },
82 composable(Routes.SESSIONS) {
85 container = container,
86 onOpenFiles = { navController.navigate(Routes.FILES) },
87 onOpenSettings = { navController.navigate(Routes.SETTINGS) },
89 scope.launch { container.disconnect() }
90 navController.popBackStack(Routes.SERVERS, inclusive = false)
95 container = container,
96 onOpenSession = { id ->
97 container.consumePendingShare()?.let { scope.launch { container.draftStore.set(id, it) } }
98 navController.navigate(Routes.chat(id))
101 scope.launch { container.disconnect() }
102 navController.popBackStack(Routes.SERVERS, inclusive = false)
104 onOpenFiles = { navController.navigate(Routes.FILES) },
105 onOpenSettings = { navController.navigate(Routes.SETTINGS) },
111 route = "${Routes.CHAT}/{sessionId}",
112 arguments = listOf(navArgument("sessionId") { type = NavType.StringType }),
115 container = container,
116 sessionId = entry.arguments?.getString("sessionId").orEmpty(),
117 onBack = { navController.popBackStack() },
121 composable(Routes.FILES) {
123 container = container,
124 onOpenFile = { path -> navController.navigate(Routes.fileView(path)) },
125 onBack = { navController.popBackStack() },
130 route = "${Routes.FILE_VIEW}?path={path}",
131 arguments = listOf(navArgument("path") { type = NavType.StringType; defaultValue = "" }),
134 container = container,
135 path = entry.arguments?.getString("path").orEmpty(),
136 onBack = { navController.popBackStack() },
140 composable(Routes.SETTINGS) {
142 container = container,
143 onBack = { navController.popBackStack() },
145 navController.popBackStack(Routes.SERVERS, inclusive = false)
147 onOpenDiagnostics = { navController.navigate(Routes.DIAGNOSTICS) },
151 composable(Routes.DIAGNOSTICS) {
152 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