Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 171 additions & 1 deletion packages/app/src/components/settings-general.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, createMemo, type JSX } from "solid-js"
import { Component, createMemo, createSignal, Show, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { Button } from "@opencode-ai/ui/button"
import { Select } from "@opencode-ai/ui/select"
Expand All @@ -8,6 +8,7 @@ import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
import { useSettings, monoFontFamily } from "@/context/settings"
import { useGlobalSDK } from "@/context/global-sdk"
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"

Expand Down Expand Up @@ -35,6 +36,53 @@ export const SettingsGeneral: Component = () => {
const language = useLanguage()
const platform = usePlatform()
const settings = useSettings()
const globalSDK = useGlobalSDK()

// YOLO state - wird später vom Server geladen
const [yoloEnabled, setYoloEnabled] = createSignal(false)
const [yoloPersisted, setYoloPersisted] = createSignal(false)

// Lade YOLO status beim Öffnen - mit kleinem Delay für Stabilität
const loadYoloStatus = () => {
const doFetch = platform.fetch ?? fetch
doFetch(`${globalSDK.url}/config/yolo`)
.then((response) => {
if (response.ok) return response.json()
return null
})
.then((data) => {
if (data) {
setYoloEnabled(data.enabled === true)
setYoloPersisted(data.persisted === true)
}
})
.catch(() => {
// Silently ignore errors
})
}

// Initialer Load mit kleinem Delay
setTimeout(loadYoloStatus, 100)

const setYolo = (enabled: boolean, persist: boolean) => {
const doFetch = platform.fetch ?? fetch
doFetch(`${globalSDK.url}/config/yolo`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled, persist }),
})
.then((response) => {
if (response.ok) return response.json()
return null
})
.then((data) => {
if (data) {
setYoloEnabled(data.enabled === true)
setYoloPersisted(data.persisted === true)
}
})
.catch((e) => console.error("Failed to set YOLO:", e))
}

const [store, setStore] = createStore({
checking: false,
Expand Down Expand Up @@ -410,6 +458,128 @@ export const SettingsGeneral: Component = () => {
</SettingsRow>
</div>
</div>

{/* YOLO Mode Section */}
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2 pb-2">
<h3 class="text-14-medium text-text-strong">YOLO Mode</h3>
<Show when={yoloEnabled()}>
<span class="text-10-medium text-fill-danger-base bg-fill-danger-ghost px-1.5 py-0.5 rounded">
ACTIVE
</span>
</Show>
</div>

<p class="text-12-regular text-text-muted pb-2">
Skip ALL permission prompts. OpenCode will execute without asking for confirmation.
</p>

{/* Warning */}
<div class="p-3 rounded-lg border border-fill-warning-base bg-fill-warning-ghost mb-3">
<p class="text-12-regular text-text-base">
<span class="text-12-medium">Warning:</span> This is dangerous. Only enable if you fully trust OpenCode's
actions. Explicit deny rules in your config will still be respected.
</p>
</div>

{/* This Session Only Card */}
<div
class="p-3 rounded-lg border-2 transition-all mb-2"
classList={{
"border-fill-danger-base bg-fill-danger-ghost": yoloEnabled() && !yoloPersisted(),
"border-border-weak-base bg-fill-ghost-base": !(yoloEnabled() && !yoloPersisted()),
}}
>
<div class="flex items-center justify-between">
<div class="flex flex-col gap-0.5">
<div class="flex items-center gap-2">
<span class="text-13-medium text-text-base">This Session Only</span>
<Show when={yoloEnabled() && !yoloPersisted()}>
<span class="text-10-medium text-fill-danger-base">ACTIVE</span>
</Show>
</div>
<span class="text-11-regular text-text-muted">Resets when you restart OpenCode</span>
</div>
<Show
when={yoloEnabled() && !yoloPersisted()}
fallback={
<button
type="button"
onClick={() => setYolo(true, false)}
class="px-3 py-1.5 rounded text-12-medium bg-fill-danger-base text-white hover:bg-fill-danger-strong transition-colors"
>
Enable
</button>
}
>
<button
type="button"
onClick={() => setYolo(false, false)}
class="px-3 py-1.5 rounded border border-border-base text-12-medium text-text-base hover:bg-fill-ghost-base transition-colors"
>
Disable
</button>
</Show>
</div>
</div>

{/* Always Enabled Card */}
<div
class="p-3 rounded-lg border-2 transition-all mb-3"
classList={{
"border-fill-danger-base bg-fill-danger-ghost": yoloPersisted(),
"border-border-weak-base bg-fill-ghost-base": !yoloPersisted(),
}}
>
<div class="flex items-center justify-between">
<div class="flex flex-col gap-0.5">
<div class="flex items-center gap-2">
<span class="text-13-medium text-text-base">Always Enabled</span>
<Show when={yoloPersisted()}>
<span class="text-10-medium text-fill-danger-base">ACTIVE</span>
<span class="text-10-medium text-fill-success-base">Saved</span>
</Show>
</div>
<span class="text-11-regular text-text-muted">Persists across restarts (saved in config.json)</span>
</div>
<Show
when={yoloPersisted()}
fallback={
<button
type="button"
onClick={() => setYolo(true, true)}
class="px-3 py-1.5 rounded text-12-medium bg-fill-danger-base text-white hover:bg-fill-danger-strong transition-colors"
>
Save to Config
</button>
}
>
<button
type="button"
onClick={() => setYolo(false, true)}
class="px-3 py-1.5 rounded border border-border-base text-12-medium text-text-base hover:bg-fill-ghost-base transition-colors"
>
Remove from Config
</button>
</Show>
</div>
</div>

{/* CLI Usage */}
<div class="p-3 rounded-lg border border-border-weak-base bg-fill-ghost-base">
<span class="text-12-medium text-text-base">CLI Usage</span>
<div class="mt-2 flex flex-col gap-1">
<div class="flex items-center gap-3">
<code class="text-11-regular font-mono text-text-muted">opencode --yolo</code>
<span class="text-11-regular text-text-weak">one session</span>
</div>
<div class="flex items-center gap-3">
<code class="text-11-regular font-mono text-text-muted">OPENCODE_YOLO=true</code>
<span class="text-11-regular text-text-weak">env var</span>
</div>
</div>
</div>
</div>
</div>
</div>
)
Expand Down
4 changes: 4 additions & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,10 @@ export namespace Config {
prune: z.boolean().optional().describe("Enable pruning of old tool outputs (default: true)"),
})
.optional(),
yolo: z
.boolean()
.optional()
.describe("Enable YOLO mode - auto-approve all permission prompts (except explicit deny rules)"),
experimental: z
.object({
disable_paste_summary: z.boolean().optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/flag/flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ function truthy(key: string) {
}

export namespace Flag {
export const OPENCODE_YOLO = truthy("OPENCODE_YOLO")
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
export const OPENCODE_GIT_BASH_PATH = process.env["OPENCODE_GIT_BASH_PATH"]
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/permission/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Storage } from "@/storage/storage"
import { fn } from "@/util/fn"
import { Log } from "@/util/log"
import { Wildcard } from "@/util/wildcard"
import { Yolo } from "@/yolo"
import os from "os"
import z from "zod"

Expand Down Expand Up @@ -137,6 +138,11 @@ export namespace PermissionNext {
if (rule.action === "deny")
throw new DeniedError(ruleset.filter((r) => Wildcard.match(request.permission, r.permission)))
if (rule.action === "ask") {
// YOLO mode auto-approves all "ask" permissions (but respects explicit "deny")
if (Yolo.isEnabled()) {
log.warn("YOLO mode auto-approved", { permission: request.permission, pattern })
continue
}
const id = input.id ?? Identifier.ascending("permission")
return new Promise<void>((resolve, reject) => {
const info: Request = {
Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/project/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import { Log } from "@/util/log"
import { ShareNext } from "@/share/share-next"
import { Snapshot } from "../snapshot"
import { Truncate } from "../tool/truncation"
import { Yolo } from "../yolo"

export async function InstanceBootstrap() {
Log.Default.info("bootstrapping", { directory: Instance.directory })
await Yolo.init()
await Plugin.init()
Share.init()
ShareNext.init()
Expand Down
96 changes: 96 additions & 0 deletions packages/opencode/src/server/routes/config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
import { Hono } from "hono"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import path from "path"
import { Config } from "../../config/config"
import { Provider } from "../../provider/provider"
import { Yolo } from "../../yolo"
import { Global } from "../../global"
import { mapValues } from "remeda"
import { errors } from "../error"
import { Log } from "../../util/log"
import { lazy } from "../../util/lazy"

const log = Log.create({ service: "server" })

// Helper to read/write global config for YOLO persistence (uses config.json, not opencode.jsonc)
async function readGlobalConfig(): Promise<Record<string, unknown>> {
const filepath = path.join(Global.Path.config, "config.json")
try {
const text = await Bun.file(filepath).text()
return JSON.parse(text)
} catch {
return {}
}
}

async function writeGlobalConfig(config: Record<string, unknown>): Promise<void> {
const filepath = path.join(Global.Path.config, "config.json")
await Bun.write(filepath, JSON.stringify(config, null, 2))
}

export const ConfigRoutes = lazy(() =>
new Hono()
.get(
Expand Down Expand Up @@ -88,5 +107,82 @@ export const ConfigRoutes = lazy(() =>
default: mapValues(providers, (item) => Provider.sort(Object.values(item.models))[0].id),
})
},
)
.get(
"/yolo",
describeRoute({
summary: "Get YOLO mode status",
description:
"Check if YOLO mode is enabled. When enabled, all permission prompts are auto-approved (except explicit deny rules).",
operationId: "config.yolo.get",
responses: {
200: {
description: "YOLO mode status",
content: {
"application/json": {
schema: resolver(z.object({ enabled: z.boolean(), persisted: z.boolean() })),
},
},
},
},
}),
async (c) => {
const globalConfig = await readGlobalConfig()
return c.json({
enabled: Yolo.isEnabled(),
persisted: globalConfig.yolo === true,
})
},
)
.post(
"/yolo",
describeRoute({
summary: "Set YOLO mode",
description:
"Enable or disable YOLO mode. When enabled, all permission prompts are auto-approved (except explicit deny rules). Use with caution. Set persist=true to save to config file.",
operationId: "config.yolo.set",
responses: {
200: {
description: "YOLO mode updated",
content: {
"application/json": {
schema: resolver(z.object({ enabled: z.boolean(), persisted: z.boolean() })),
},
},
},
},
}),
validator("json", z.object({ enabled: z.boolean(), persist: z.boolean().optional() })),
async (c) => {
const { enabled, persist } = c.req.valid("json")
Yolo.set(enabled)

try {
const globalConfig = await readGlobalConfig()
const wasPersisted = globalConfig.yolo === true

if (persist) {
// Explicitly save to or remove from config
if (enabled) {
globalConfig.yolo = true
} else {
delete globalConfig.yolo
}
await writeGlobalConfig(globalConfig)
log.info("YOLO mode config updated", { enabled, path: Global.Path.config })
} else if (wasPersisted && enabled) {
// Downgrade from persistent to session-only: remove from config but keep enabled
delete globalConfig.yolo
await writeGlobalConfig(globalConfig)
log.info("YOLO mode downgraded to session-only", { path: Global.Path.config })
}
} catch (e) {
log.error("Failed to update YOLO config", { error: e })
}

// Return the actual persisted state from config
const finalConfig = await readGlobalConfig()
return c.json({ enabled: Yolo.isEnabled(), persisted: finalConfig.yolo === true })
},
),
)
Loading
Loading