1package soy.iko.opencode.ui
3import androidx.activity.compose.BackHandler
4import androidx.compose.animation.AnimatedContent
5import androidx.compose.animation.slideInHorizontally
6import androidx.compose.animation.slideOutHorizontally
7import androidx.compose.animation.togetherWith
8import androidx.compose.foundation.layout.fillMaxSize
9import androidx.compose.foundation.layout.padding
10import androidx.compose.material3.AlertDialog
11import androidx.compose.material3.Scaffold
12import androidx.compose.material3.SnackbarDuration
13import androidx.compose.material3.SnackbarHost
14import androidx.compose.material3.SnackbarHostState
15import androidx.compose.material3.Text
16import androidx.compose.material3.TextButton
17import androidx.compose.runtime.Composable
18import androidx.compose.runtime.LaunchedEffect
19import androidx.compose.runtime.getValue
20import androidx.compose.runtime.mutableStateOf
21import androidx.compose.runtime.remember
22import androidx.compose.runtime.setValue
23import androidx.compose.ui.Modifier
24import soy.iko.opencode.core.Event
25import soy.iko.opencode.core.Screen
26import soy.iko.opencode.core.UiState
27import soy.iko.opencode.ui.screens.ChatScreen
28import soy.iko.opencode.ui.screens.ConnectScreen
29import soy.iko.opencode.ui.screens.SessionsScreen
32fun OpencodeApp(state: UiState, dispatch: (Event) -> Unit) {
33 val snackbar = remember { SnackbarHostState() }
34 var showStopConfirm by remember { mutableStateOf(false) }
35 var showDisconnectConfirm by remember { mutableStateOf(false) }
37 LaunchedEffect(state.error) {
39 snackbar.showSnackbar(
41 actionLabel = "Dismiss",
42 duration = SnackbarDuration.Short,
44 // Always clear the error from state after showing.
45 dispatch(Event.DismissError)
49 BackHandler(enabled = state.screen != Screen.Connect) {
51 // Leaving mid-generation would silently drop the reply, so confirm first.
52 Screen.Chat -> if (state.generating) showStopConfirm = true else dispatch(Event.NavigateToSessions)
53 // Disconnecting drops the server session, so guard the back button.
54 Screen.Sessions -> showDisconnectConfirm = true
55 Screen.Connect -> Unit
59 if (showStopConfirm) {
61 onDismissRequest = { showStopConfirm = false },
62 title = { Text("Stop generation?") },
63 text = { Text("Leaving will stop the current generation.") },
65 TextButton(onClick = {
66 showStopConfirm = false
67 dispatch(Event.CancelGeneration)
68 dispatch(Event.NavigateToSessions)
70 Text("Stop and leave")
74 TextButton(onClick = { showStopConfirm = false }) {
81 if (showDisconnectConfirm) {
83 onDismissRequest = { showDisconnectConfirm = false },
84 title = { Text("Disconnect?") },
85 text = { Text("You will be disconnected from the server.") },
87 TextButton(onClick = {
88 showDisconnectConfirm = false
89 dispatch(Event.NavigateToConnect)
95 TextButton(onClick = { showDisconnectConfirm = false }) {
102 Scaffold(snackbarHost = { SnackbarHost(snackbar) }) { padding ->
104 targetState = state.screen,
105 modifier = Modifier.fillMaxSize().padding(padding),
107 // Compare screen ordinals to decide slide direction: forward navigation
108 // (Connect → Sessions → Chat) slides in from the right, back slides from the left.
109 if (targetState.ordinal > initialState.ordinal) {
110 slideInHorizontally { it } togetherWith slideOutHorizontally { -it }
112 slideInHorizontally { -it } togetherWith slideOutHorizontally { it }
118 Screen.Connect -> ConnectScreen(state, dispatch)
119 Screen.Sessions -> SessionsScreen(state, dispatch)
120 Screen.Chat -> ChatScreen(state, dispatch)
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