mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
tui: allow users to respond to and track permission requests for granular approval control
This commit is contained in:
parent
3f699a91e1
commit
a317048d3d
4 changed files with 101 additions and 3 deletions
|
|
@ -4,7 +4,6 @@ import { TextAttributes } from "@opentui/core"
|
|||
import { RouteProvider, useRoute } from "@tui/context/route"
|
||||
import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
|
||||
import { Installation } from "@/installation"
|
||||
import { Global } from "@/global"
|
||||
import { Flag } from "@/flag/flag"
|
||||
import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
||||
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
||||
|
|
@ -34,6 +33,7 @@ import { Provider } from "@/provider/provider"
|
|||
import { ArgsProvider, useArgs, type Args } from "./context/args"
|
||||
import open from "open"
|
||||
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
||||
import { Permission } from "./component/dialog-permission"
|
||||
|
||||
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
||||
// can't set raw mode if not a TTY
|
||||
|
|
@ -573,6 +573,7 @@ function App() {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<Permission />
|
||||
<Switch>
|
||||
<Match when={route.data.type === "home"}>
|
||||
<Home />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { onMount } from "solid-js"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { TextAttributes } from "@opentui/core"
|
||||
import { useTheme } from "../context/theme"
|
||||
|
||||
export function Permission() {
|
||||
const dialog = useDialog()
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
dialog.replace(() => <DialogPermission />)
|
||||
}, 2000)
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
function DialogPermission() {
|
||||
const dialog = useDialog()
|
||||
const { theme } = useTheme()
|
||||
|
||||
onMount(() => {
|
||||
dialog.setSize("medium")
|
||||
})
|
||||
|
||||
return (
|
||||
<box
|
||||
gap={1}
|
||||
paddingLeft={2}
|
||||
paddingRight={2}
|
||||
onKeyDown={(e) => {
|
||||
console.log(e)
|
||||
}}
|
||||
ref={(r) => {
|
||||
setTimeout(() => {
|
||||
r?.focus()
|
||||
}, 1)
|
||||
}}
|
||||
>
|
||||
<box flexDirection="row" justifyContent="space-between">
|
||||
<text attributes={TextAttributes.BOLD}>Permission Request</text>
|
||||
<text fg={theme.textMuted}>esc</text>
|
||||
</box>
|
||||
<text fg={theme.textMuted}>Change to foo directory and create bar file</text>
|
||||
<text>$ cd foo && touch bar</text>
|
||||
<box paddingBottom={1}>
|
||||
<box paddingLeft={2} paddingRight={2} backgroundColor={theme.primary}>
|
||||
<text fg={theme.background}>Allow</text>
|
||||
</box>
|
||||
<box paddingLeft={2} paddingRight={2}>
|
||||
<text>Always allow the touch command</text>
|
||||
</box>
|
||||
<box paddingLeft={2} paddingRight={2}>
|
||||
<text>Reject</text>
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
)
|
||||
}
|
||||
|
|
@ -99,6 +99,7 @@ function init() {
|
|||
replace(input: any, onClose?: () => void) {
|
||||
if (store.stack.length === 0) {
|
||||
focus = renderer.currentFocusedRenderable
|
||||
focus?.blur()
|
||||
}
|
||||
for (const item of store.stack) {
|
||||
if (item.onClose) item.onClose()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Bus } from "@/bus"
|
||||
import { Identifier } from "@/id/id"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { fn } from "@/util/fn"
|
||||
|
|
@ -7,6 +8,8 @@ export namespace PermissionNext {
|
|||
export const Info = z
|
||||
.object({
|
||||
id: Identifier.schema("permission"),
|
||||
sessionID: Identifier.schema("session"),
|
||||
type: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
keys: z.string().array(),
|
||||
|
|
@ -21,23 +24,38 @@ export namespace PermissionNext {
|
|||
export const Response = z.enum(["once", "always", "reject"])
|
||||
export type Response = z.infer<typeof Response>
|
||||
|
||||
export const Approval = z.object({
|
||||
projectID: z.string(),
|
||||
patterns: z.string().array(),
|
||||
})
|
||||
|
||||
export const Event = {
|
||||
Updated: Bus.event("permission.next.updated", Info),
|
||||
}
|
||||
|
||||
const state = Instance.state(() => {
|
||||
const pending: Record<
|
||||
string,
|
||||
{
|
||||
info: Info
|
||||
resolve: (info: Info) => void
|
||||
resolve: () => void
|
||||
reject: (e: any) => void
|
||||
}
|
||||
> = {}
|
||||
|
||||
const approved: {
|
||||
[projectID: string]: Set<string>
|
||||
} = {}
|
||||
|
||||
return {
|
||||
pending,
|
||||
approved,
|
||||
}
|
||||
})
|
||||
|
||||
export const ask = fn(Info.partial({ id: true }), async (input) => {
|
||||
const id = input.id ?? Identifier.ascending("permission")
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const s = state()
|
||||
s.pending[id] = {
|
||||
info: {
|
||||
|
|
@ -50,6 +68,27 @@ export namespace PermissionNext {
|
|||
})
|
||||
})
|
||||
|
||||
export const respond = fn(
|
||||
z.object({
|
||||
permissionID: Identifier.schema("permission"),
|
||||
response: Response,
|
||||
}),
|
||||
async (input) => {
|
||||
const existing = state().pending[input.permissionID]
|
||||
if (!existing) return
|
||||
if (input.response === "reject") {
|
||||
existing.reject(new RejectedError())
|
||||
return
|
||||
}
|
||||
if (input.response === "once") {
|
||||
existing.resolve()
|
||||
return
|
||||
}
|
||||
if (input.response === "always") {
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export class RejectedError extends Error {
|
||||
constructor(public readonly reason?: string) {
|
||||
super(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue