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(