diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index c53ca04e2383..46099649e238 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -63,6 +63,7 @@ export namespace Agent { question: "deny", plan_enter: "deny", plan_exit: "deny", + edit: "ask", // mirrors github.com/github/gitignore Node.gitignore pattern for .env files read: { "*": "allow", diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index bf63eabf8155..2cdae139b890 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -360,6 +360,11 @@ export const RunCommand = cmd({ action: "deny", pattern: "*", }, + { + permission: "edit", + action: "allow", + pattern: "*", + }, ] function title() { diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index ab3d09689252..792756fa05d0 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -457,6 +457,7 @@ function App() { { title: "Toggle MCPs", value: "mcp.list", + search: "toggle mcps", category: "Agent", slash: { name: "mcps", @@ -532,8 +533,9 @@ function App() { category: "System", }, { - title: "Toggle appearance", + title: mode() === "dark" ? "Light mode" : "Dark mode", value: "theme.switch_mode", + search: "toggle appearance", onSelect: (dialog) => { setMode(mode() === "dark" ? "light" : "dark") dialog.clear() @@ -572,6 +574,7 @@ function App() { }, { title: "Toggle debug panel", + search: "toggle debug", category: "System", value: "app.debug", onSelect: (dialog) => { @@ -581,6 +584,7 @@ function App() { }, { title: "Toggle console", + search: "toggle console", category: "System", value: "app.console", onSelect: (dialog) => { @@ -621,6 +625,7 @@ function App() { { title: terminalTitleEnabled() ? "Disable terminal title" : "Enable terminal title", value: "terminal.title.toggle", + search: "toggle terminal title", keybind: "terminal_title_toggle", category: "System", onSelect: (dialog) => { @@ -636,6 +641,7 @@ function App() { { title: kv.get("animations_enabled", true) ? "Disable animations" : "Enable animations", value: "app.toggle.animations", + search: "toggle animations", category: "System", onSelect: (dialog) => { kv.set("animations_enabled", !kv.get("animations_enabled", true)) @@ -645,6 +651,7 @@ function App() { { title: kv.get("diff_wrap_mode", "word") === "word" ? "Disable diff wrapping" : "Enable diff wrapping", value: "app.toggle.diffwrap", + search: "toggle diff wrapping", category: "System", onSelect: (dialog) => { const current = kv.get("diff_wrap_mode", "word") diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index c30b8d12a933..b210f8ed353e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -7,6 +7,27 @@ import { useDialog } from "@tui/ui/dialog" import { createDialogProviderOptions, DialogProvider } from "./dialog-provider" import { useKeybind } from "../context/keybind" import * as fuzzysort from "fuzzysort" +import type { Provider } from "@opencode-ai/sdk/v2" + +function pickLatest(models: [string, Provider["models"][string]][]) { + const picks: Record = {} + for (const item of models) { + const model = item[0] + const info = item[1] + const key = info.family ?? model + const prev = picks[key] + if (!prev) { + picks[key] = item + continue + } + if (info.release_date !== prev[1].release_date) { + if (info.release_date > prev[1].release_date) picks[key] = item + continue + } + if (model > prev[0]) picks[key] = item + } + return Object.values(picks) +} export function useConnected() { const sync = useSync() @@ -21,6 +42,7 @@ export function DialogModel(props: { providerID?: string }) { const dialog = useDialog() const keybind = useKeybind() const [query, setQuery] = createSignal("") + const [all, setAll] = createSignal(false) const connected = useConnected() const providers = createDialogProviderOptions() @@ -72,8 +94,8 @@ export function DialogModel(props: { providerID?: string }) { (provider) => provider.id !== "opencode", (provider) => provider.name, ), - flatMap((provider) => - pipe( + flatMap((provider) => { + const items = pipe( provider.models, entries(), filter(([_, info]) => info.status !== "deprecated"), @@ -104,8 +126,9 @@ export function DialogModel(props: { providerID?: string }) { (x) => x.footer !== "Free", (x) => x.title, ), - ), - ), + ) + return items + }), ) const popularProviders = !connected() @@ -154,6 +177,13 @@ export function DialogModel(props: { providerID?: string }) { local.model.toggleFavorite(option.value as { providerID: string; modelID: string }) }, }, + { + keybind: keybind.all.model_show_all_toggle?.[0], + title: all() ? "Show latest only" : "Show all models", + onTrigger: () => { + setAll((value) => !value) + }, + }, ]} onFilter={setQuery} flat={true} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 4114daf6c636..b9ebd63f4cfe 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -75,6 +75,7 @@ export function Prompt(props: PromptProps) { const renderer = useRenderer() const { theme, syntax } = useTheme() const kv = useKV() + const [autoaccept, setAutoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit") function promptModelWarning() { toast.show({ @@ -168,6 +169,17 @@ export function Prompt(props: PromptProps) { command.register(() => { return [ + { + title: autoaccept() === "none" ? "Enable autoedit" : "Disable autoedit", + value: "permission.auto_accept.toggle", + search: "toggle permissions", + keybind: "permission_auto_accept_toggle", + category: "Agent", + onSelect: (dialog) => { + setAutoaccept(() => (autoaccept() === "none" ? "edit" : "none")) + dialog.clear() + }, + }, { title: "Clear prompt", value: "prompt.clear", @@ -994,23 +1006,30 @@ export function Prompt(props: PromptProps) { cursorColor={theme.text} syntaxStyle={syntax()} /> - - - {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} - - - - - {local.model.parsed().model} - - {local.model.parsed().provider} - - · - - {local.model.variant.current()} + + + + {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "} + + + + + {local.model.parsed().model} - - + {local.model.parsed().provider} + + · + + {local.model.variant.current()} + + + + + + + + autoedit + diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 269ed7ae0bd1..270a422c6dd4 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -25,6 +25,7 @@ import { createSimpleContext } from "./helper" import type { Snapshot } from "@/snapshot" import { useExit } from "./exit" import { useArgs } from "./args" +import { useKV } from "./kv" import { batch, onMount } from "solid-js" import { Log } from "@/util/log" import type { Path } from "@opencode-ai/sdk" @@ -103,6 +104,8 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ }) const sdk = useSDK() + const kv = useKV() + const [autoaccept] = kv.signal<"none" | "edit">("permission_auto_accept", "edit") sdk.event.listen((e) => { const event = e.details @@ -127,6 +130,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ case "permission.asked": { const request = event.properties + if (autoaccept() === "edit" && request.permission === "edit") { + sdk.client.permission.reply({ + reply: "once", + requestID: request.id, + }) + break + } const requests = store.permission[request.sessionID] if (!requests) { setStore("permission", request.sessionID, [request]) @@ -441,6 +451,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ get ready() { return store.status !== "loading" }, + session: { get(sessionID: string) { const match = Binary.search(store.session, sessionID, (s) => s.id) diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index c011f6c62468..9da0a9d5e3ed 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -46,6 +46,7 @@ export function Home() { { title: tipsHidden() ? "Show tips" : "Hide tips", value: "tips.toggle", + search: "toggle tips", keybind: "tips_toggle", category: "System", onSelect: (dialog) => { diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 55ab4d54dd4c..a4f97ee71b9e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -523,6 +523,7 @@ export function Session() { { title: sidebarVisible() ? "Hide sidebar" : "Show sidebar", value: "session.sidebar.toggle", + search: "toggle sidebar", keybind: "sidebar_toggle", category: "Session", onSelect: (dialog) => { @@ -537,6 +538,7 @@ export function Session() { { title: conceal() ? "Disable code concealment" : "Enable code concealment", value: "session.toggle.conceal", + search: "toggle code concealment", keybind: "messages_toggle_conceal" as any, category: "Session", onSelect: (dialog) => { @@ -547,6 +549,7 @@ export function Session() { { title: showTimestamps() ? "Hide timestamps" : "Show timestamps", value: "session.toggle.timestamps", + search: "toggle timestamps", category: "Session", slash: { name: "timestamps", @@ -560,6 +563,7 @@ export function Session() { { title: showThinking() ? "Hide thinking" : "Show thinking", value: "session.toggle.thinking", + search: "toggle thinking", keybind: "display_thinking", category: "Session", slash: { @@ -574,6 +578,7 @@ export function Session() { { title: showDetails() ? "Hide tool details" : "Show tool details", value: "session.toggle.actions", + search: "toggle tool details", keybind: "tool_details", category: "Session", onSelect: (dialog) => { @@ -582,8 +587,9 @@ export function Session() { }, }, { - title: "Toggle session scrollbar", + title: showScrollbar() ? "Hide session scrollbar" : "Show session scrollbar", value: "session.toggle.scrollbar", + search: "toggle session scrollbar", keybind: "scrollbar_toggle", category: "Session", onSelect: (dialog) => { @@ -1378,6 +1384,7 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) { const ctx = use() const { theme, syntax } = useTheme() + const isStreaming = () => !props.message.time.completed return ( @@ -1385,7 +1392,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess @@ -1394,7 +1401,7 @@ function TextPart(props: { last: boolean; part: TextPart; message: AssistantMess { title: string value: T description?: string + search?: string footer?: JSX.Element | string category?: string disabled?: boolean @@ -85,8 +86,8 @@ export function DialogSelect(props: DialogSelectProps) { // users typically search by the item name, and not its category. const result = fuzzysort .go(needle, options, { - keys: ["title", "category"], - scoreFn: (r) => r[0].score * 2 + r[1].score, + keys: ["title", "category", "search"], + scoreFn: (r) => r[0].score * 2 + r[1].score + r[2].score, }) .map((x) => x.obj) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index dfdcb0343ef3..5832816b24e8 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -780,6 +780,7 @@ export namespace Config { stash_delete: z.string().optional().default("ctrl+d").describe("Delete stash entry"), model_provider_list: z.string().optional().default("ctrl+a").describe("Open provider list from model dialog"), model_favorite_toggle: z.string().optional().default("ctrl+f").describe("Toggle model favorite status"), + model_show_all_toggle: z.string().optional().default("ctrl+o").describe("Toggle showing all models"), session_share: z.string().optional().default("none").describe("Share current session"), session_unshare: z.string().optional().default("none").describe("Unshare current session"), session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"), @@ -820,7 +821,12 @@ export namespace Config { command_list: z.string().optional().default("ctrl+p").describe("List available commands"), agent_list: z.string().optional().default("a").describe("List agents"), agent_cycle: z.string().optional().default("tab").describe("Next agent"), - agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), + agent_cycle_reverse: z.string().optional().default("none").describe("Previous agent"), + permission_auto_accept_toggle: z + .string() + .optional() + .default("shift+tab") + .describe("Toggle auto-accept mode for permissions"), variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), input_clear: z.string().optional().default("ctrl+c").describe("Clear input field"), input_paste: z.string().optional().default("ctrl+v").describe("Paste from clipboard"), diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 0049d716d095..43f75a47693f 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -50,7 +50,7 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_LSP_TY = truthy("OPENCODE_EXPERIMENTAL_LSP_TY") export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL") export const OPENCODE_DISABLE_FILETIME_CHECK = truthy("OPENCODE_DISABLE_FILETIME_CHECK") - export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") + export const OPENCODE_EXPERIMENTAL_MARKDOWN = truthy("OPENCODE_EXPERIMENTAL_MARKDOWN") export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"] export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"] diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 43ad9a09d399..02db2f68a72e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1322,33 +1322,7 @@ export namespace SessionPrompt { const userMessage = input.messages.findLast((msg) => msg.info.role === "user") if (!userMessage) return input.messages - // Original logic when experimental plan mode is disabled - if (!Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE) { - if (input.agent.name === "plan") { - userMessage.parts.push({ - id: Identifier.ascending("part"), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: PROMPT_PLAN, - synthetic: true, - }) - } - const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") - if (wasPlan && input.agent.name === "build") { - userMessage.parts.push({ - id: Identifier.ascending("part"), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: BUILD_SWITCH, - synthetic: true, - }) - } - return input.messages - } - - // New plan mode logic when flag is enabled + // Plan mode logic const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") // Switching from plan mode to build mode diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 3ff9cce8990f..fec12f42d683 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -115,7 +115,7 @@ export namespace ToolRegistry { ApplyPatchTool, ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []), ...(config.experimental?.batch_tool === true ? [BatchTool] : []), - ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []), + ...(Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []), ...custom, ] } diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 9f8de04f80c4..a3b3a2d77294 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -38,7 +38,7 @@ test("build agent has correct default properties", async () => { expect(build).toBeDefined() expect(build?.mode).toBe("primary") expect(build?.native).toBe(true) - expect(evalPerm(build, "edit")).toBe("allow") + expect(evalPerm(build, "edit")).toBe("ask") expect(evalPerm(build, "bash")).toBe("allow") }, }) @@ -217,8 +217,8 @@ test("agent permission config merges with defaults", async () => { expect(build).toBeDefined() // Specific pattern is denied expect(PermissionNext.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny") - // Edit still allowed - expect(evalPerm(build, "edit")).toBe("allow") + // Edit still asks (default behavior) + expect(evalPerm(build, "edit")).toBe("ask") }, }) }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index efb7e202e120..044df2584109 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1067,6 +1067,10 @@ export type KeybindsConfig = { * Toggle model favorite status */ model_favorite_toggle?: string + /** + * Toggle showing all models + */ + model_show_all_toggle?: string /** * Share current session */ @@ -1183,6 +1187,10 @@ export type KeybindsConfig = { * Previous agent */ agent_cycle_reverse?: string + /** + * Toggle auto-accept mode for permissions + */ + permission_auto_accept_toggle?: string /** * Cycle model variants */ diff --git a/packages/web/src/content/docs/cli.mdx b/packages/web/src/content/docs/cli.mdx index c504f734fa5d..b4f453a0f8f8 100644 --- a/packages/web/src/content/docs/cli.mdx +++ b/packages/web/src/content/docs/cli.mdx @@ -600,4 +600,3 @@ These environment variables enable experimental features that may change or be r | `OPENCODE_EXPERIMENTAL_EXA` | boolean | Enable experimental Exa features | | `OPENCODE_EXPERIMENTAL_LSP_TY` | boolean | Enable experimental LSP type checking | | `OPENCODE_EXPERIMENTAL_MARKDOWN` | boolean | Enable experimental markdown features | -| `OPENCODE_EXPERIMENTAL_PLAN_MODE` | boolean | Enable plan mode |