diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index fad09c3a7add..f7787e970577 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -233,7 +233,14 @@ export const RunCommand = effectCmd({ hidden: true, describe: "cap visible interactive replay to the newest N messages", }) - .option("dangerously-skip-permissions", { + .option("interactive", { + alias: ["i"], + type: "boolean", + describe: "run in direct interactive split-footer mode", + default: false, + }) + .option("auto", { + alias: ["yolo", "dangerously-skip-permissions"], type: "boolean", describe: "auto-approve permissions that are not explicitly denied (dangerous!)", default: false, @@ -780,7 +787,7 @@ export const RunCommand = effectCmd({ const permission = event.properties if (permission.sessionID !== sessionID) continue - if (args["dangerously-skip-permissions"]) { + if (args.auto) { await client.permission.reply({ requestID: permission.id, reply: "once", @@ -981,9 +988,12 @@ export async function runMini(input: MiniCommandInput) { variant: undefined, thinking: undefined, mini: true, + interactive: false, replay: input.replay ?? true, "replay-limit": input.replayLimit, replayLimit: input.replayLimit, + auto: false, + yolo: false, "dangerously-skip-permissions": false, dangerouslySkipPermissions: false, demo: input.demo ?? false, diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index a4443d2b03d9..7b92919d6185 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -105,6 +105,12 @@ export const TuiThreadCommand = cmd({ type: "string", describe: "agent to use", }) + .option("auto", { + alias: ["yolo", "dangerously-skip-permissions"], + type: "boolean", + describe: "auto-approve permissions that are not explicitly denied (dangerous!)", + default: false, + }) .option("mini", { type: "boolean", describe: "start the minimal interactive interface", @@ -272,6 +278,7 @@ export const TuiThreadCommand = cmd({ model: args.model, prompt, fork: args.fork, + auto: args.auto, }, }), ) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index fecf71b6c8c8..5788a947a11d 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -37,6 +37,7 @@ import { SyncProvider, useSync } from "./context/sync" import { DataProvider } from "./context/data" import { LocationProvider } from "./context/location" import { LocalProvider, useLocal } from "./context/local" +import { PermissionProvider } from "./context/permission" import { DialogModel } from "./component/dialog-model" import { useConnected } from "./component/use-connected" import { DialogMcp } from "./component/dialog-mcp" @@ -296,34 +297,36 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) { headers={input.headers} events={input.events} > - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + @@ -926,6 +929,16 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi dialog.clear() }, }, + { + name: "permission.mode", + title: + local.permission.mode === "auto" ? "Disable auto-approve permissions" : "Enable auto-approve permissions", + category: "System", + run: () => { + local.permission.toggle() + dialog.clear() + }, + }, ].map((command) => ({ namespace: "palette", ...command, diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx index 0b87df9241b6..0fb04c5c108b 100644 --- a/packages/tui/src/component/prompt/index.tsx +++ b/packages/tui/src/component/prompt/index.tsx @@ -1442,6 +1442,9 @@ export function Prompt(props: PromptProps) { {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)} + + auto + ยท diff --git a/packages/tui/src/context/args.tsx b/packages/tui/src/context/args.tsx index 8a229ffaba69..9a528f2f0ece 100644 --- a/packages/tui/src/context/args.tsx +++ b/packages/tui/src/context/args.tsx @@ -7,6 +7,7 @@ export interface Args { continue?: boolean sessionID?: string fork?: boolean + auto?: boolean } export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({ diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index 39e7bc111d55..a05d315166ed 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -12,6 +12,7 @@ import { readJson, writeJsonAtomic } from "../util/persistence" import { useTheme } from "./theme" import { useToast } from "../ui/toast" import { useRoute } from "./route" +import { usePermission } from "./permission" export type LocalTheme = { secondary: RGBA @@ -56,6 +57,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const theme = useTheme().theme const route = useRoute() const paths = useTuiPaths() + const args = useArgs() + const event = useEvent() + const permission = usePermission() function isModelValid(model: { providerID: string; modelID: string }) { const provider = sync.data.provider.find((item) => item.id === model.providerID) @@ -190,7 +194,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (state.pending) save() }) - const args = useArgs() const fallbackModel = createMemo(() => { if (args.model) { const { providerID, modelID } = parseModel(args.model) @@ -446,8 +449,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (state.pending) save() }) - const event = useEvent() - const slots = createMemo(() => { const existing = new Set(sync.data.session.filter((x) => x.parentID === undefined).map((x) => x.id)) return sessionStore.pinned.filter((id) => existing.has(id)).slice(0, 9) @@ -534,6 +535,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ agent, mcp, session, + permission, } return result }, diff --git a/packages/tui/src/context/permission.tsx b/packages/tui/src/context/permission.tsx new file mode 100644 index 000000000000..13a6ba822339 --- /dev/null +++ b/packages/tui/src/context/permission.tsx @@ -0,0 +1,26 @@ +import { createStore } from "solid-js/store" +import { useArgs } from "./args" +import { createSimpleContext } from "./helper" + +export type PermissionMode = "auto" | "normal" + +export const { use: usePermission, provider: PermissionProvider } = createSimpleContext({ + name: "Permission", + init: () => { + const args = useArgs() + const [store, setStore] = createStore<{ mode: PermissionMode }>({ + mode: args.auto ? "auto" : "normal", + }) + return { + get mode() { + return store.mode + }, + set(mode: PermissionMode) { + setStore("mode", mode) + }, + toggle() { + setStore("mode", (mode) => (mode === "auto" ? "normal" : "auto")) + }, + } + }, +}) diff --git a/packages/tui/src/context/sync.tsx b/packages/tui/src/context/sync.tsx index 03db8784de49..d0511c5183e4 100644 --- a/packages/tui/src/context/sync.tsx +++ b/packages/tui/src/context/sync.tsx @@ -31,6 +31,7 @@ import { useArgs } from "./args" import { batch, onMount } from "solid-js" import path from "path" import { useKV } from "./kv" +import { usePermission } from "./permission" const emptyConsoleState: ConsoleState = { consoleManagedProviders: [], @@ -59,6 +60,7 @@ export const { init: () => { const startup = useTuiStartup() const kv = useKV() + const permission = usePermission() const [store, setStore] = createStore<{ status: "loading" | "partial" | "complete" provider: Provider[] @@ -165,7 +167,7 @@ export const { .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) } - event.subscribe((event, { workspace }) => { + event.subscribe((event, { directory, workspace }) => { switch (event.type) { case "server.instance.disposed": void bootstrap() @@ -187,6 +189,15 @@ export const { case "permission.asked": { const request = event.properties + if (permission.mode === "auto") { + void sdk.client.permission.reply({ + requestID: request.id, + reply: "once", + directory, + workspace, + }) + break + } const requests = store.permission[request.sessionID] if (!requests) { setStore("permission", request.sessionID, [request]) diff --git a/packages/tui/test/cli/cmd/tui/sync-fixture.tsx b/packages/tui/test/cli/cmd/tui/sync-fixture.tsx index 5b7872ee3070..f81c4401a61a 100644 --- a/packages/tui/test/cli/cmd/tui/sync-fixture.tsx +++ b/packages/tui/test/cli/cmd/tui/sync-fixture.tsx @@ -6,6 +6,7 @@ import { KVProvider, useKV } from "../../../../src/context/kv" import { ProjectProvider, useProject } from "../../../../src/context/project" import { SDKProvider } from "../../../../src/context/sdk" import { SyncProvider, useSync } from "../../../../src/context/sync" +import { PermissionProvider } from "../../../../src/context/permission" import { ExitProvider } from "../../../../src/context/exit" import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk" import { TestTuiContexts } from "../../../fixture/tui-environment" @@ -48,13 +49,15 @@ export async function mount(override?: FetchHandler, state?: string) { - - {}}> - - - - - + + + {}}> + + + + + + diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index 0f9e5e3b1f48..b44a27f4e1d2 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -37,6 +37,7 @@ opencode [project] | {"--prompt"} | | Prompt to use | | {"--model"} | `-m` | Model to use in the form of provider/model | | {"--agent"} | | Agent to use | +| {"--auto"} | | Auto-approve permissions that are not explicitly denied | | {"--port"} | | Port to listen on | | {"--hostname"} | | Hostname to listen on | | {"--mdns"} | | Enable mDNS discovery | @@ -380,7 +381,7 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript" | {"--port"} | | Port for the local server (defaults to random port) | | {"--variant"} | | Model variant (provider-specific reasoning effort) | | {"--thinking"} | | Show thinking blocks | -| {"--dangerously-skip-permissions"} | | Auto-approve permissions that are not explicitly denied (dangerous!) | +| {"--auto"} | | Auto-approve permissions that are not explicitly denied | --- diff --git a/packages/web/src/content/docs/permissions.mdx b/packages/web/src/content/docs/permissions.mdx index 19182e27491b..ccc4034dc696 100644 --- a/packages/web/src/content/docs/permissions.mdx +++ b/packages/web/src/content/docs/permissions.mdx @@ -19,6 +19,26 @@ Each permission rule resolves to one of: --- +## Auto mode + +Start OpenCode with `--auto` to automatically approve permission requests that are not explicitly denied. + +```bash +opencode --auto +``` + +You can also use auto mode with [`opencode run`](/docs/cli#run). + +```bash +opencode run --auto "Refactor this module" +``` + +Explicit `"deny"` rules are still enforced. Auto mode only changes requests that would otherwise ask for approval. + +In the TUI, open the command palette and select **Enable auto-approve permissions** or **Disable auto-approve permissions** to change modes. When auto mode is active, the prompt displays a muted `auto` indicator next to the current agent. + +--- + ## Configuration You can set permissions globally (with `*`), and override specific tools.