feat(tui): add yolo permission mode (#33279)

Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
Dax
2026-06-30 00:08:29 -05:00
committed by GitHub
co-authored by Aiden Cline
parent ba41dadc91
commit 0a5bed2bc2
11 changed files with 133 additions and 36 deletions
+12 -2
View File
@@ -233,7 +233,14 @@ export const RunCommand = effectCmd({
hidden: true, hidden: true,
describe: "cap visible interactive replay to the newest N messages", 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", type: "boolean",
describe: "auto-approve permissions that are not explicitly denied (dangerous!)", describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
default: false, default: false,
@@ -780,7 +787,7 @@ export const RunCommand = effectCmd({
const permission = event.properties const permission = event.properties
if (permission.sessionID !== sessionID) continue if (permission.sessionID !== sessionID) continue
if (args["dangerously-skip-permissions"]) { if (args.auto) {
await client.permission.reply({ await client.permission.reply({
requestID: permission.id, requestID: permission.id,
reply: "once", reply: "once",
@@ -981,9 +988,12 @@ export async function runMini(input: MiniCommandInput) {
variant: undefined, variant: undefined,
thinking: undefined, thinking: undefined,
mini: true, mini: true,
interactive: false,
replay: input.replay ?? true, replay: input.replay ?? true,
"replay-limit": input.replayLimit, "replay-limit": input.replayLimit,
replayLimit: input.replayLimit, replayLimit: input.replayLimit,
auto: false,
yolo: false,
"dangerously-skip-permissions": false, "dangerously-skip-permissions": false,
dangerouslySkipPermissions: false, dangerouslySkipPermissions: false,
demo: input.demo ?? false, demo: input.demo ?? false,
+7
View File
@@ -105,6 +105,12 @@ export const TuiThreadCommand = cmd({
type: "string", type: "string",
describe: "agent to use", 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", { .option("mini", {
type: "boolean", type: "boolean",
describe: "start the minimal interactive interface", describe: "start the minimal interactive interface",
@@ -272,6 +278,7 @@ export const TuiThreadCommand = cmd({
model: args.model, model: args.model,
prompt, prompt,
fork: args.fork, fork: args.fork,
auto: args.auto,
}, },
}), }),
) )
+35 -22
View File
@@ -37,6 +37,7 @@ import { SyncProvider, useSync } from "./context/sync"
import { DataProvider } from "./context/data" import { DataProvider } from "./context/data"
import { LocationProvider } from "./context/location" import { LocationProvider } from "./context/location"
import { LocalProvider, useLocal } from "./context/local" import { LocalProvider, useLocal } from "./context/local"
import { PermissionProvider } from "./context/permission"
import { DialogModel } from "./component/dialog-model" import { DialogModel } from "./component/dialog-model"
import { useConnected } from "./component/use-connected" import { useConnected } from "./component/use-connected"
import { DialogMcp } from "./component/dialog-mcp" import { DialogMcp } from "./component/dialog-mcp"
@@ -296,34 +297,36 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
headers={input.headers} headers={input.headers}
events={input.events} events={input.events}
> >
<ProjectProvider> <PermissionProvider>
<SyncProvider> <ProjectProvider>
<DataProvider> <SyncProvider>
<ThemeProvider mode={mode}> <DataProvider>
<LocalProvider> <ThemeProvider mode={mode}>
<PromptStashProvider> <LocalProvider>
<DialogProvider> <PromptStashProvider>
<FrecencyProvider> <DialogProvider>
<PromptHistoryProvider> <FrecencyProvider>
<PromptRefProvider> <PromptHistoryProvider>
<EditorContextProvider> <PromptRefProvider>
<EditorContextProvider>
<LocationProvider> <LocationProvider>
<App <App
onSnapshot={input.onSnapshot} onSnapshot={input.onSnapshot}
pluginHost={input.pluginHost} pluginHost={input.pluginHost}
/> />
</LocationProvider> </LocationProvider>
</EditorContextProvider> </EditorContextProvider>
</PromptRefProvider> </PromptRefProvider>
</PromptHistoryProvider> </PromptHistoryProvider>
</FrecencyProvider> </FrecencyProvider>
</DialogProvider> </DialogProvider>
</PromptStashProvider> </PromptStashProvider>
</LocalProvider> </LocalProvider>
</ThemeProvider> </ThemeProvider>
</DataProvider> </DataProvider>
</SyncProvider> </SyncProvider>
</ProjectProvider> </ProjectProvider>
</PermissionProvider>
</SDKProvider> </SDKProvider>
</PluginRuntimeProvider> </PluginRuntimeProvider>
</TuiConfigProvider> </TuiConfigProvider>
@@ -926,6 +929,16 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
dialog.clear() 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) => ({ ].map((command) => ({
namespace: "palette", namespace: "palette",
...command, ...command,
@@ -1442,6 +1442,9 @@ export function Prompt(props: PromptProps) {
<text fg={fadeColor(highlight(), agentMetaAlpha())}> <text fg={fadeColor(highlight(), agentMetaAlpha())}>
{store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)} {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}
</text> </text>
<Show when={store.mode === "normal" && local.permission.mode === "auto"}>
<text fg={fadeColor(theme.textMuted, agentMetaAlpha())}>auto</text>
</Show>
<Show when={store.mode === "normal"}> <Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>·</text> <text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>·</text>
+1
View File
@@ -7,6 +7,7 @@ export interface Args {
continue?: boolean continue?: boolean
sessionID?: string sessionID?: string
fork?: boolean fork?: boolean
auto?: boolean
} }
export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({ export const { use: useArgs, provider: ArgsProvider } = createSimpleContext({
+5 -3
View File
@@ -12,6 +12,7 @@ import { readJson, writeJsonAtomic } from "../util/persistence"
import { useTheme } from "./theme" import { useTheme } from "./theme"
import { useToast } from "../ui/toast" import { useToast } from "../ui/toast"
import { useRoute } from "./route" import { useRoute } from "./route"
import { usePermission } from "./permission"
export type LocalTheme = { export type LocalTheme = {
secondary: RGBA secondary: RGBA
@@ -56,6 +57,9 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
const theme = useTheme().theme const theme = useTheme().theme
const route = useRoute() const route = useRoute()
const paths = useTuiPaths() const paths = useTuiPaths()
const args = useArgs()
const event = useEvent()
const permission = usePermission()
function isModelValid(model: { providerID: string; modelID: string }) { function isModelValid(model: { providerID: string; modelID: string }) {
const provider = sync.data.provider.find((item) => item.id === model.providerID) 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() if (state.pending) save()
}) })
const args = useArgs()
const fallbackModel = createMemo(() => { const fallbackModel = createMemo(() => {
if (args.model) { if (args.model) {
const { providerID, modelID } = parseModel(args.model) const { providerID, modelID } = parseModel(args.model)
@@ -446,8 +449,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
if (state.pending) save() if (state.pending) save()
}) })
const event = useEvent()
const slots = createMemo(() => { const slots = createMemo(() => {
const existing = new Set(sync.data.session.filter((x) => x.parentID === undefined).map((x) => x.id)) 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) return sessionStore.pinned.filter((id) => existing.has(id)).slice(0, 9)
@@ -534,6 +535,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
agent, agent,
mcp, mcp,
session, session,
permission,
} }
return result return result
}, },
+26
View File
@@ -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"))
},
}
},
})
+12 -1
View File
@@ -31,6 +31,7 @@ import { useArgs } from "./args"
import { batch, onMount } from "solid-js" import { batch, onMount } from "solid-js"
import path from "path" import path from "path"
import { useKV } from "./kv" import { useKV } from "./kv"
import { usePermission } from "./permission"
const emptyConsoleState: ConsoleState = { const emptyConsoleState: ConsoleState = {
consoleManagedProviders: [], consoleManagedProviders: [],
@@ -59,6 +60,7 @@ export const {
init: () => { init: () => {
const startup = useTuiStartup() const startup = useTuiStartup()
const kv = useKV() const kv = useKV()
const permission = usePermission()
const [store, setStore] = createStore<{ const [store, setStore] = createStore<{
status: "loading" | "partial" | "complete" status: "loading" | "partial" | "complete"
provider: Provider[] provider: Provider[]
@@ -165,7 +167,7 @@ export const {
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
} }
event.subscribe((event, { workspace }) => { event.subscribe((event, { directory, workspace }) => {
switch (event.type) { switch (event.type) {
case "server.instance.disposed": case "server.instance.disposed":
void bootstrap() void bootstrap()
@@ -187,6 +189,15 @@ export const {
case "permission.asked": { case "permission.asked": {
const request = event.properties 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] const requests = store.permission[request.sessionID]
if (!requests) { if (!requests) {
setStore("permission", request.sessionID, [request]) setStore("permission", request.sessionID, [request])
+10 -7
View File
@@ -6,6 +6,7 @@ import { KVProvider, useKV } from "../../../../src/context/kv"
import { ProjectProvider, useProject } from "../../../../src/context/project" import { ProjectProvider, useProject } from "../../../../src/context/project"
import { SDKProvider } from "../../../../src/context/sdk" import { SDKProvider } from "../../../../src/context/sdk"
import { SyncProvider, useSync } from "../../../../src/context/sync" import { SyncProvider, useSync } from "../../../../src/context/sync"
import { PermissionProvider } from "../../../../src/context/permission"
import { ExitProvider } from "../../../../src/context/exit" import { ExitProvider } from "../../../../src/context/exit"
import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk" import { createEventSource, createFetch, type FetchHandler, directory } from "../../../fixture/tui-sdk"
import { TestTuiContexts } from "../../../fixture/tui-environment" import { TestTuiContexts } from "../../../fixture/tui-environment"
@@ -48,13 +49,15 @@ export async function mount(override?: FetchHandler, state?: string) {
<ArgsProvider> <ArgsProvider>
<KVProvider> <KVProvider>
<SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}> <SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
<ProjectProvider> <PermissionProvider>
<ExitProvider exit={() => {}}> <ProjectProvider>
<SyncProvider> <ExitProvider exit={() => {}}>
<Probe /> <SyncProvider>
</SyncProvider> <Probe />
</ExitProvider> </SyncProvider>
</ProjectProvider> </ExitProvider>
</ProjectProvider>
</PermissionProvider>
</SDKProvider> </SDKProvider>
</KVProvider> </KVProvider>
</ArgsProvider> </ArgsProvider>
+2 -1
View File
@@ -37,6 +37,7 @@ opencode [project]
| <nobr><code>{"--prompt"}</code></nobr> | | Prompt to use | | <nobr><code>{"--prompt"}</code></nobr> | | Prompt to use |
| <nobr><code>{"--model"}</code></nobr> | `-m` | Model to use in the form of provider/model | | <nobr><code>{"--model"}</code></nobr> | `-m` | Model to use in the form of provider/model |
| <nobr><code>{"--agent"}</code></nobr> | | Agent to use | | <nobr><code>{"--agent"}</code></nobr> | | Agent to use |
| <nobr><code>{"--auto"}</code></nobr> | | Auto-approve permissions that are not explicitly denied |
| <nobr><code>{"--port"}</code></nobr> | | Port to listen on | | <nobr><code>{"--port"}</code></nobr> | | Port to listen on |
| <nobr><code>{"--hostname"}</code></nobr> | | Hostname to listen on | | <nobr><code>{"--hostname"}</code></nobr> | | Hostname to listen on |
| <nobr><code>{"--mdns"}</code></nobr> | | Enable mDNS discovery | | <nobr><code>{"--mdns"}</code></nobr> | | Enable mDNS discovery |
@@ -380,7 +381,7 @@ opencode run --attach http://localhost:4096 "Explain async/await in JavaScript"
| <nobr><code>{"--port"}</code></nobr> | | Port for the local server (defaults to random port) | | <nobr><code>{"--port"}</code></nobr> | | Port for the local server (defaults to random port) |
| <nobr><code>{"--variant"}</code></nobr> | | Model variant (provider-specific reasoning effort) | | <nobr><code>{"--variant"}</code></nobr> | | Model variant (provider-specific reasoning effort) |
| <nobr><code>{"--thinking"}</code></nobr> | | Show thinking blocks | | <nobr><code>{"--thinking"}</code></nobr> | | Show thinking blocks |
| <nobr><code>{"--dangerously-skip-permissions"}</code></nobr> | | Auto-approve permissions that are not explicitly denied (dangerous!) | | <nobr><code>{"--auto"}</code></nobr> | | Auto-approve permissions that are not explicitly denied |
--- ---
@@ -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 ## Configuration
You can set permissions globally (with `*`), and override specific tools. You can set permissions globally (with `*`), and override specific tools.