1package soy.iko.opencode.ui.chat
3import androidx.compose.foundation.layout.Arrangement
4import androidx.compose.foundation.layout.Column
5import androidx.compose.foundation.layout.Row
6import androidx.compose.foundation.layout.fillMaxWidth
7import androidx.compose.foundation.layout.padding
8import androidx.compose.material.icons.Icons
9import androidx.compose.material.icons.filled.Shield
10import androidx.compose.material3.AlertDialog
11import androidx.compose.material3.Button
12import androidx.compose.material3.Icon
13import androidx.compose.material3.MaterialTheme
14import androidx.compose.material3.OutlinedButton
15import androidx.compose.material3.Text
16import androidx.compose.material3.TextButton
17import androidx.compose.runtime.Composable
18import androidx.compose.runtime.mutableStateOf
19import androidx.compose.runtime.remember
20import androidx.compose.runtime.saveable.rememberSaveable
21import androidx.compose.runtime.getValue
22import androidx.compose.runtime.setValue
23import androidx.compose.ui.Modifier
24import androidx.compose.ui.platform.LocalHapticFeedback
25import androidx.compose.ui.res.stringResource
26import androidx.compose.ui.text.font.FontFamily
27import androidx.compose.ui.unit.dp
28import soy.iko.opencode.data.model.Permission
29import soy.iko.opencode.data.model.PermissionResponse
30import soy.iko.opencode.R
33 * Modal asking the user to approve a tool the agent wants to run. The agent run is
34 * paused until one of these responses is sent. Non-dismissable by tap-outside so the
35 * decision is explicit.
39 permission: Permission,
40 onRespond: (PermissionResponse) -> Unit,
42 val haptics = LocalHapticFeedback.current
43 // Guard against double-respond: back press + button tap, or rapid double-tap,
44 // could call onRespond twice. Once a response is sent, subsequent calls are no-ops.
45 // Keyed on permission.id: the host mounts this dialog against a conflating StateFlow,
46 // so a null -> permissionB transition can collapse to just permissionB, reusing this
47 // composable for a different permission. Without the key `responded` would stay true
48 // and every button (and back-press) would be a no-op — an undismissable modal.
49 var responded by rememberSaveable(permission.id) { mutableStateOf(false) }
50 val respond: (PermissionResponse) -> Unit = { response ->
57 // Back press routes here (dismissOnBackPress defaults true) and is treated as an
58 // explicit reject — the safe default. Tap-outside is disabled below so it can't
59 // dismiss silently. A host-composition BackHandler doesn't work: AlertDialog
60 // renders in its own window whose back dispatcher never reaches the host.
61 onDismissRequest = { respond(PermissionResponse.REJECT) },
62 properties = androidx.compose.ui.window.DialogProperties(dismissOnClickOutside = false),
63 icon = { Icon(Icons.Filled.Shield, contentDescription = null) },
64 title = { Text(permission.title ?: stringResource(R.string.permission_title)) },
66 Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
67 permission.type?.let {
68 Text(it, style = MaterialTheme.typography.labelLarge, fontFamily = FontFamily.Monospace)
70 permission.patternText?.let {
73 style = MaterialTheme.typography.bodyMedium,
74 fontFamily = FontFamily.Monospace,
75 color = MaterialTheme.colorScheme.onSurfaceVariant,
79 stringResource(R.string.permission_text),
80 style = MaterialTheme.typography.bodySmall,
81 color = MaterialTheme.colorScheme.onSurfaceVariant,
87 modifier = Modifier.fillMaxWidth(),
88 verticalArrangement = Arrangement.spacedBy(4.dp),
92 haptics.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress)
93 respond(PermissionResponse.ONCE)
95 modifier = Modifier.fillMaxWidth(),
96 ) { Text(stringResource(R.string.allow_once)) }
99 haptics.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress)
100 respond(PermissionResponse.ALWAYS)
102 modifier = Modifier.fillMaxWidth(),
103 ) { Text(stringResource(R.string.always_allow)) }
104 // Reject is an OutlinedButton (not a low-prominence TextButton) in the
105 // same column so the three actions are visually balanced — the prior
106 // layout buried Reject as a small dismiss-button, nudging users toward
107 // granting. Reject uses the error color to signal its consequence.
110 haptics.performHapticFeedback(androidx.compose.ui.hapticfeedback.HapticFeedbackType.LongPress)
111 respond(PermissionResponse.REJECT)
113 modifier = Modifier.fillMaxWidth(),
114 ) { Text(stringResource(R.string.reject), color = MaterialTheme.colorScheme.error) }
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