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.data.network.NetworkConfig
18import soy.iko.opencode.ui.chat.ChatScreen
19import soy.iko.opencode.ui.file.FileBrowserScreen
20import soy.iko.opencode.ui.file.FileViewScreen
21import soy.iko.opencode.ui.server.ServerEditScreen
22import soy.iko.opencode.ui.server.ServerListScreen
23import soy.iko.opencode.ui.session.SessionListScreen
24import soy.iko.opencode.ui.session.TwoPaneSessionChat
25import soy.iko.opencode.ui.settings.DiagnosticsScreen
26import soy.iko.opencode.ui.settings.SettingsScreen
27import soy.iko.opencode.util.runCatchingCancellable
30fun OpencodeApp(container: AppContainer) {
31 val navController = rememberNavController()
32 val scope = rememberCoroutineScope()
33 val pendingShare by container.pendingShare.collectAsStateWithLifecycle()
34 val pendingOpenSession by container.pendingOpenSession.collectAsStateWithLifecycle()
35 val connection by container.activeConnection.collectAsStateWithLifecycle()
37 // When text is shared into the app, surface the session list so the user can pick
38 // (or create) a conversation to drop it into. The chosen session's draft is set
39 // in [onOpenSession] below. Keyed on `connection` too so a share that arrives
40 // before auto-reconnect completes is retried once the connection is established.
41 LaunchedEffect(pendingShare, connection) {
42 if (pendingShare == null) return@LaunchedEffect
43 if (connection == null) return@LaunchedEffect
44 if (!navController.popBackStack(Routes.SESSIONS, inclusive = false)) {
45 navController.navigate(Routes.SESSIONS) { launchSingleTop = true }
49 // Adaptive: on wide screens (tablets / unfolded foldables) show the session list and
50 // the open conversation side by side instead of a single-pane back stack.
52 val isTwoPane = maxWidth >= NetworkConfig.twoPaneWidthThresholdDp.dp && connection != null
54 // Open a session requested by a notification tap or deep link, once connected.
55 // In two-pane mode the detail pane consumes the request instead of navigating.
56 LaunchedEffect(pendingOpenSession, connection, isTwoPane) {
57 val id = pendingOpenSession ?: return@LaunchedEffect
58 if (connection == null || isTwoPane) return@LaunchedEffect
59 container.consumePendingOpenSession()
60 navController.navigate(Routes.chat(id)) { launchSingleTop = true }
63 NavHost(navController = navController, startDestination = Routes.SERVERS) {
65 composable(Routes.SERVERS) {
67 container = container,
68 onConnected = { navController.navigate(Routes.SESSIONS) },
69 onAddProfile = { navController.navigate(Routes.serverEdit()) },
70 onEditProfile = { id -> navController.navigate(Routes.serverEdit(id)) },
75 route = "${Routes.SERVER_EDIT}?id={id}",
76 arguments = listOf(navArgument("id") { type = NavType.StringType; nullable = true; defaultValue = null }),
79 container = container,
80 profileId = entry.arguments?.getString("id"),
81 onDone = { navController.popBackStack() },
85 composable(Routes.SESSIONS) {
88 container = container,
89 onOpenFiles = { navController.navigate(Routes.FILES) },
90 onOpenSettings = { navController.navigate(Routes.SETTINGS) },
92 scope.launch { runCatchingCancellable { container.disconnect() } }
93 navController.popBackStack(Routes.SERVERS, inclusive = false)
98 container = container,
99 onOpenSession = { id ->
100 // Set the draft synchronously in-memory before navigating so the
101 // ChatScreen sees it on first composition. The async persistence
102 // to disk happens in the background via draftStore.set.
103 container.consumePendingShare()?.let { shareText ->
104 container.draftStore.setImmediate(id, shareText)
105 scope.launch { runCatchingCancellable { container.draftStore.set(id, shareText) } }
107 navController.navigate(Routes.chat(id))
110 scope.launch { runCatchingCancellable { container.disconnect() } }
111 navController.popBackStack(Routes.SERVERS, inclusive = false)
113 onOpenFiles = { navController.navigate(Routes.FILES) },
114 onOpenSettings = { navController.navigate(Routes.SETTINGS) },
120 route = "${Routes.CHAT}/{sessionId}",
121 arguments = listOf(navArgument("sessionId") { type = NavType.StringType }),
124 container = container,
125 sessionId = entry.arguments?.getString("sessionId").orEmpty(),
126 onBack = { navController.popBackStack() },
130 composable(Routes.FILES) {
132 container = container,
133 onOpenFile = { path -> navController.navigate(Routes.fileView(path)) },
134 onBack = { navController.popBackStack() },
139 route = "${Routes.FILE_VIEW}?path={path}",
140 arguments = listOf(navArgument("path") { type = NavType.StringType; defaultValue = "" }),
143 container = container,
144 path = entry.arguments?.getString("path").orEmpty(),
145 onBack = { navController.popBackStack() },
149 composable(Routes.SETTINGS) {
151 container = container,
152 onBack = { navController.popBackStack() },
154 navController.popBackStack(Routes.SERVERS, inclusive = false)
156 onOpenDiagnostics = { navController.navigate(Routes.DIAGNOSTICS) },
160 composable(Routes.DIAGNOSTICS) {
161 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