diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index e81de1889..4b2e7bf3e 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -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() { } }} > + diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-permission.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-permission.tsx new file mode 100644 index 000000000..91be5c093 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-permission.tsx @@ -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(() => ) + }, 2000) + }) + return null +} + +function DialogPermission() { + const dialog = useDialog() + const { theme } = useTheme() + + onMount(() => { + dialog.setSize("medium") + }) + + return ( + { + console.log(e) + }} + ref={(r) => { + setTimeout(() => { + r?.focus() + }, 1) + }} + > + + Permission Request + esc + + Change to foo directory and create bar file + $ cd foo && touch bar + + + Allow + + + Always allow the touch command + + + Reject + + + + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 9b773111c..79bca4240 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -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() diff --git a/packages/opencode/src/permission/next.ts b/packages/opencode/src/permission/next.ts index 928e06b9c..ddb11ac7a 100644 --- a/packages/opencode/src/permission/next.ts +++ b/packages/opencode/src/permission/next.ts @@ -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 + 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 + } = {} + 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((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(