tui: add reject message support to permission dialogs for better user feedback
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createMemo, For, Match, Show, Switch } from "solid-js"
|
||||
import { useKeyboard, useTerminalDimensions, type JSX } from "@opentui/solid"
|
||||
import type { TextareaRenderable } from "@opentui/core"
|
||||
import { useKeybind } from "../../context/keybind"
|
||||
import { useTheme } from "../../context/theme"
|
||||
import type { PermissionRequest } from "@opencode-ai/sdk/v2"
|
||||
import { useSDK } from "../../context/sdk"
|
||||
import { SplitBorder } from "../../component/border"
|
||||
import { useSync } from "../../context/sync"
|
||||
import { useTextareaKeybindings } from "../../component/textarea-keybindings"
|
||||
import path from "path"
|
||||
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
|
||||
import { Locale } from "@/util/locale"
|
||||
|
||||
type PermissionStage = "permission" | "always" | "reject"
|
||||
|
||||
function normalizePath(input?: string) {
|
||||
if (!input) return ""
|
||||
if (path.isAbsolute(input)) {
|
||||
@@ -101,9 +105,11 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
const sdk = useSDK()
|
||||
const sync = useSync()
|
||||
const [store, setStore] = createStore({
|
||||
always: false,
|
||||
stage: "permission" as PermissionStage,
|
||||
})
|
||||
|
||||
const session = createMemo(() => sync.data.session.find((s) => s.id === props.request.sessionID))
|
||||
|
||||
const input = createMemo(() => {
|
||||
const tool = props.request.tool
|
||||
if (!tool) return {}
|
||||
@@ -120,7 +126,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={store.always}>
|
||||
<Match when={store.stage === "always"}>
|
||||
<Prompt
|
||||
title="Always allow"
|
||||
body={
|
||||
@@ -148,7 +154,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
options={{ confirm: "Confirm", cancel: "Cancel" }}
|
||||
escapeKey="cancel"
|
||||
onSelect={(option) => {
|
||||
setStore("always", false)
|
||||
setStore("stage", "permission")
|
||||
if (option === "cancel") return
|
||||
sdk.client.permission.reply({
|
||||
reply: "always",
|
||||
@@ -157,7 +163,19 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
}}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={!store.always}>
|
||||
<Match when={store.stage === "reject"}>
|
||||
<RejectPrompt
|
||||
onConfirm={(message) => {
|
||||
sdk.client.permission.reply({
|
||||
reply: "reject",
|
||||
requestID: props.request.id,
|
||||
message: message || undefined,
|
||||
})
|
||||
}}
|
||||
onCancel={() => setStore("stage", "permission")}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={store.stage === "permission"}>
|
||||
<Prompt
|
||||
title="Permission required"
|
||||
body={
|
||||
@@ -215,11 +233,21 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
escapeKey="reject"
|
||||
onSelect={(option) => {
|
||||
if (option === "always") {
|
||||
setStore("always", true)
|
||||
setStore("stage", "always")
|
||||
return
|
||||
}
|
||||
if (option === "reject") {
|
||||
if (session()?.parentID) {
|
||||
setStore("stage", "reject")
|
||||
return
|
||||
}
|
||||
sdk.client.permission.reply({
|
||||
reply: "reject",
|
||||
requestID: props.request.id,
|
||||
})
|
||||
}
|
||||
sdk.client.permission.reply({
|
||||
reply: option as "once" | "reject",
|
||||
reply: "once",
|
||||
requestID: props.request.id,
|
||||
})
|
||||
}}
|
||||
@@ -229,6 +257,71 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
|
||||
)
|
||||
}
|
||||
|
||||
function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: () => void }) {
|
||||
let input: TextareaRenderable
|
||||
const { theme } = useTheme()
|
||||
const keybind = useKeybind()
|
||||
const textareaKeybindings = useTextareaKeybindings()
|
||||
|
||||
useKeyboard((evt) => {
|
||||
if (evt.name === "escape" || keybind.match("app_exit", evt)) {
|
||||
evt.preventDefault()
|
||||
props.onCancel()
|
||||
return
|
||||
}
|
||||
if (evt.name === "return") {
|
||||
evt.preventDefault()
|
||||
props.onConfirm(input.plainText)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<box
|
||||
backgroundColor={theme.backgroundPanel}
|
||||
border={["left"]}
|
||||
borderColor={theme.error}
|
||||
customBorderChars={SplitBorder.customBorderChars}
|
||||
>
|
||||
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1}>
|
||||
<box flexDirection="row" gap={1} paddingLeft={1}>
|
||||
<text fg={theme.error}>{"△"}</text>
|
||||
<text fg={theme.text}>Reject permission</text>
|
||||
</box>
|
||||
<box paddingLeft={1}>
|
||||
<text fg={theme.textMuted}>Tell OpenCode what to do differently</text>
|
||||
</box>
|
||||
</box>
|
||||
<box
|
||||
flexDirection="row"
|
||||
flexShrink={0}
|
||||
paddingTop={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={3}
|
||||
paddingBottom={1}
|
||||
backgroundColor={theme.backgroundElement}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<textarea
|
||||
ref={(val: TextareaRenderable) => (input = val)}
|
||||
focused
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
cursorColor={theme.primary}
|
||||
keyBindings={textareaKeybindings()}
|
||||
/>
|
||||
<box flexDirection="row" gap={2} flexShrink={0} marginLeft={1}>
|
||||
<text fg={theme.text}>
|
||||
enter <span style={{ fg: theme.textMuted }}>confirm</span>
|
||||
</text>
|
||||
<text fg={theme.text}>
|
||||
esc <span style={{ fg: theme.textMuted }}>cancel</span>
|
||||
</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
||||
function Prompt<const T extends Record<string, string>>(props: {
|
||||
title: string
|
||||
body: JSX.Element
|
||||
|
||||
Reference in New Issue
Block a user