feat(tui): add yolo permission mode (#33279)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -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])
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user