فهرست منبع

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

Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Dax 1 ماه پیش
والد
کامیت
0a5bed2bc2

+ 12 - 2
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,

+ 7 - 0
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,
             },
           }),
         )

+ 35 - 22
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}
                                         >
-                                          <ProjectProvider>
-                                            <SyncProvider>
-                                              <DataProvider>
-                                                <ThemeProvider mode={mode}>
-                                                  <LocalProvider>
-                                                    <PromptStashProvider>
-                                                      <DialogProvider>
-                                                        <FrecencyProvider>
-                                                          <PromptHistoryProvider>
-                                                            <PromptRefProvider>
-                                                              <EditorContextProvider>
+                                          <PermissionProvider>
+                                            <ProjectProvider>
+                                              <SyncProvider>
+                                                <DataProvider>
+                                                  <ThemeProvider mode={mode}>
+                                                    <LocalProvider>
+                                                      <PromptStashProvider>
+                                                        <DialogProvider>
+                                                          <FrecencyProvider>
+                                                            <PromptHistoryProvider>
+                                                              <PromptRefProvider>
+                                                                <EditorContextProvider>
                                                                 <LocationProvider>
                                                                   <App
                                                                     onSnapshot={input.onSnapshot}
                                                                     pluginHost={input.pluginHost}
                                                                   />
                                                                 </LocationProvider>
-                                                              </EditorContextProvider>
-                                                            </PromptRefProvider>
-                                                          </PromptHistoryProvider>
-                                                        </FrecencyProvider>
-                                                      </DialogProvider>
-                                                    </PromptStashProvider>
-                                                  </LocalProvider>
-                                                </ThemeProvider>
-                                              </DataProvider>
-                                            </SyncProvider>
-                                          </ProjectProvider>
+                                                                </EditorContextProvider>
+                                                              </PromptRefProvider>
+                                                            </PromptHistoryProvider>
+                                                          </FrecencyProvider>
+                                                        </DialogProvider>
+                                                      </PromptStashProvider>
+                                                    </LocalProvider>
+                                                  </ThemeProvider>
+                                                </DataProvider>
+                                              </SyncProvider>
+                                            </ProjectProvider>
+                                          </PermissionProvider>
                                         </SDKProvider>
                                       </PluginRuntimeProvider>
                                     </TuiConfigProvider>
@@ -926,6 +929,16 @@ function App(props: { onSnapshot?: () => Promise<string[]>; 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,

+ 3 - 0
packages/tui/src/component/prompt/index.tsx

@@ -1442,6 +1442,9 @@ export function Prompt(props: PromptProps) {
                       <text fg={fadeColor(highlight(), agentMetaAlpha())}>
                         {store.mode === "shell" ? "Shell" : Locale.titlecase(agent().name)}
                       </text>
+                      <Show when={store.mode === "normal" && local.permission.mode === "auto"}>
+                        <text fg={fadeColor(theme.textMuted, agentMetaAlpha())}>auto</text>
+                      </Show>
                       <Show when={store.mode === "normal"}>
                         <box flexDirection="row" gap={1}>
                           <text fg={fadeColor(theme.textMuted, modelMetaAlpha())}>·</text>

+ 1 - 0
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({

+ 5 - 3
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
   },

+ 26 - 0
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"))
+      },
+    }
+  },
+})

+ 12 - 1
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])

+ 10 - 7
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) {
       <ArgsProvider>
         <KVProvider>
           <SDKProvider url="http://test" directory={directory} fetch={calls.fetch} events={events.source}>
-            <ProjectProvider>
-              <ExitProvider exit={() => {}}>
-                <SyncProvider>
-                  <Probe />
-                </SyncProvider>
-              </ExitProvider>
-            </ProjectProvider>
+            <PermissionProvider>
+              <ProjectProvider>
+                <ExitProvider exit={() => {}}>
+                  <SyncProvider>
+                    <Probe />
+                  </SyncProvider>
+                </ExitProvider>
+              </ProjectProvider>
+            </PermissionProvider>
           </SDKProvider>
         </KVProvider>
       </ArgsProvider>

+ 2 - 1
packages/web/src/content/docs/cli.mdx

@@ -37,6 +37,7 @@ opencode [project]
 | <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>{"--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>{"--hostname"}</code></nobr>    |       | Hostname to listen on                                                   |
 | <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>{"--variant"}</code></nobr>                      |       | Model variant (provider-specific reasoning effort)                         |
 | <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                    |
 
 ---
 

+ 20 - 0
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.