From eb8ae085c5a6a3cfc6fea3e570022111d07fe59c Mon Sep 17 00:00:00 2001 From: Raine Virta Date: Mon, 13 Apr 2026 22:00:47 +0300 Subject: [PATCH 1/4] Merge branch 'pr-14442' into dev --- packages/opencode/src/cli/cmd/tui/app.tsx | 14 +++++++++++--- .../opencode/src/cli/cmd/tui/context/route.tsx | 3 ++- .../src/cli/cmd/tui/routes/session/index.tsx | 7 +++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index a58ff056485a..a209132c2599 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -329,6 +329,13 @@ function App(props: { onSnapshot?: () => Promise }) { }) const args = useArgs() + const seedInitalPrompt = () => + args.prompt + ? { + initialPrompt: { input: args.prompt, parts: [] }, + autoSubmit: true, + } + : {} onMount(() => { batch(() => { if (args.agent) local.agent.set(args.agent) @@ -346,6 +353,7 @@ function App(props: { onSnapshot?: () => Promise }) { route.navigate({ type: "session", sessionID: args.sessionID, + ...seedInitalPrompt(), }) } }) @@ -363,13 +371,13 @@ function App(props: { onSnapshot?: () => Promise }) { if (args.fork) { void sdk.client.session.fork({ sessionID: match }).then((result) => { if (result.data?.id) { - route.navigate({ type: "session", sessionID: result.data.id }) + route.navigate({ type: "session", sessionID: result.data.id, ...seedInitalPrompt() }) } else { toast.show({ message: "Failed to fork session", variant: "error" }) } }) } else { - route.navigate({ type: "session", sessionID: match }) + route.navigate({ type: "session", sessionID: match, ...seedInitalPrompt() }) } } }) @@ -383,7 +391,7 @@ function App(props: { onSnapshot?: () => Promise }) { forked = true void sdk.client.session.fork({ sessionID: args.sessionID }).then((result) => { if (result.data?.id) { - route.navigate({ type: "session", sessionID: result.data.id }) + route.navigate({ type: "session", sessionID: result.data.id, ...seedInitalPrompt() }) } else { toast.show({ message: "Failed to fork session", variant: "error" }) } diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx index 35be17801b1f..882d7734d7ac 100644 --- a/packages/opencode/src/cli/cmd/tui/context/route.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/route.tsx @@ -10,7 +10,8 @@ export type HomeRoute = { export type SessionRoute = { type: "session" sessionID: string - prompt?: PromptInfo + initialPrompt?: PromptInfo + autoSubmit?: boolean // auto-submit the prompt } export type PluginRoute = { 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 ccca4d1eba66..c3b547209ba7 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -230,9 +230,12 @@ export function Session() { const bind = (r: PromptRef | undefined) => { prompt = r promptRef.set(r) - if (seeded || !route.prompt || !r) return + if (seeded || !route.initialPrompt || !r) return seeded = true - r.set(route.prompt) + r.set(route.initialPrompt) + if (route.autoSubmit && route.initialPrompt.input) { + r.submit() + } } const keybind = useKeybind() const dialog = useDialog() From 7b2859883c6a84554f49f77228d314c87fecc66b Mon Sep 17 00:00:00 2001 From: Raine Virta Date: Mon, 13 Apr 2026 22:03:48 +0300 Subject: [PATCH 2/4] add keybind for skills search This adds a dedicated TUI keybind slot for opening the skills search popup so the existing Skills command can be triggered directly from the keyboard. The request came from issue #21758, where skills were available as a slash command and command palette action but not as a configurable keybind. The implementation wires a new `prompt_skills` key into the TUI config schema, attaches it to the existing `prompt.skills` command registration, and adds it to the keybind documentation example so users can discover and configure it. The default remains `none`, so existing behavior does not change until a user opts in. There are no breaking changes. Users who want a shortcut can now set `keybinds.prompt_skills` in `tui.json`. --- .../cli/cmd/tui/component/prompt/index.tsx | 1 + packages/opencode/src/config/config.ts | 538 ++++++++++++++++++ packages/web/src/content/docs/keybinds.mdx | 1 + 3 files changed, 540 insertions(+) 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 cf26ec1950f6..ad5b1ad5f2be 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -386,6 +386,7 @@ export function Prompt(props: PromptProps) { { title: "Skills", value: "prompt.skills", + keybind: "prompt_skills", category: "Prompt", slash: { name: "skills", diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 8980765b7934..27c413422274 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -179,6 +179,544 @@ export const Info = z z.string(), z.union([ ConfigMCP.Info, + .optional() + .describe("Maximum number of agentic iterations before forcing text-only response"), + maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."), + permission: Permission.optional(), + }) + .catchall(z.any()) + .transform((agent, ctx) => { + const knownKeys = new Set([ + "name", + "model", + "variant", + "prompt", + "description", + "temperature", + "top_p", + "mode", + "hidden", + "color", + "steps", + "maxSteps", + "options", + "permission", + "disable", + "tools", + ]) + + // Extract unknown properties into options + const options: Record = { ...agent.options } + for (const [key, value] of Object.entries(agent)) { + if (!knownKeys.has(key)) options[key] = value + } + + // Convert legacy tools config to permissions + const permission: Permission = {} + for (const [tool, enabled] of Object.entries(agent.tools ?? {})) { + const action = enabled ? "allow" : "deny" + // write, edit, patch, multiedit all map to edit permission + if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { + permission.edit = action + } else { + permission[tool] = action + } + } + Object.assign(permission, agent.permission) + + // Convert legacy maxSteps to steps + const steps = agent.steps ?? agent.maxSteps + + return { ...agent, options, permission, steps } as typeof agent & { + options?: Record + permission?: Permission + steps?: number + } + }) + .meta({ + ref: "AgentConfig", + }) + export type Agent = z.infer + + export const Keybinds = z + .object({ + leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"), + app_exit: z.string().optional().default("ctrl+c,ctrl+d,q").describe("Exit the application"), + editor_open: z.string().optional().default("e").describe("Open external editor"), + theme_list: z.string().optional().default("t").describe("List available themes"), + sidebar_toggle: z.string().optional().default("b").describe("Toggle sidebar"), + scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"), + username_toggle: z.string().optional().default("none").describe("Toggle username visibility"), + status_view: z.string().optional().default("s").describe("View status"), + session_export: z.string().optional().default("x").describe("Export session to editor"), + session_new: z.string().optional().default("n").describe("Create a new session"), + session_list: z.string().optional().default("l").describe("List all sessions"), + session_timeline: z.string().optional().default("g").describe("Show session timeline"), + session_fork: z.string().optional().default("none").describe("Fork session from message"), + session_rename: z.string().optional().default("ctrl+r").describe("Rename session"), + session_delete: z.string().optional().default("ctrl+d").describe("Delete session"), + 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"), + 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"), + session_compact: z.string().optional().default("c").describe("Compact the session"), + messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"), + messages_page_down: z + .string() + .optional() + .default("pagedown,ctrl+alt+f") + .describe("Scroll messages down by one page"), + messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"), + messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"), + messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"), + messages_half_page_down: z + .string() + .optional() + .default("ctrl+alt+d") + .describe("Scroll messages down by half page"), + messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"), + messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"), + messages_next: z.string().optional().default("none").describe("Navigate to next message"), + messages_previous: z.string().optional().default("none").describe("Navigate to previous message"), + messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"), + messages_copy: z.string().optional().default("y").describe("Copy message"), + messages_undo: z.string().optional().default("u").describe("Undo message"), + messages_redo: z.string().optional().default("r").describe("Redo message"), + messages_toggle_conceal: z + .string() + .optional() + .default("h") + .describe("Toggle code block concealment in messages"), + tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"), + model_list: z.string().optional().default("m").describe("List available models"), + model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"), + model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"), + model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"), + model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"), + 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"), + variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), + variant_list: z.string().optional().default("none").describe("List 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"), + input_submit: z.string().optional().default("return").describe("Submit input"), + input_newline: z + .string() + .optional() + .default("shift+return,ctrl+return,alt+return,ctrl+j") + .describe("Insert newline in input"), + input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"), + input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"), + input_move_up: z.string().optional().default("up").describe("Move cursor up in input"), + input_move_down: z.string().optional().default("down").describe("Move cursor down in input"), + input_select_left: z.string().optional().default("shift+left").describe("Select left in input"), + input_select_right: z.string().optional().default("shift+right").describe("Select right in input"), + input_select_up: z.string().optional().default("shift+up").describe("Select up in input"), + input_select_down: z.string().optional().default("shift+down").describe("Select down in input"), + input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"), + input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"), + input_select_line_home: z + .string() + .optional() + .default("ctrl+shift+a") + .describe("Select to start of line in input"), + input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"), + input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"), + input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"), + input_select_visual_line_home: z + .string() + .optional() + .default("alt+shift+a") + .describe("Select to start of visual line in input"), + input_select_visual_line_end: z + .string() + .optional() + .default("alt+shift+e") + .describe("Select to end of visual line in input"), + input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"), + input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"), + input_select_buffer_home: z + .string() + .optional() + .default("shift+home") + .describe("Select to start of buffer in input"), + input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"), + input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"), + input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"), + input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"), + input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"), + input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"), + input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"), + input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"), + input_word_forward: z + .string() + .optional() + .default("alt+f,alt+right,ctrl+right") + .describe("Move word forward in input"), + input_word_backward: z + .string() + .optional() + .default("alt+b,alt+left,ctrl+left") + .describe("Move word backward in input"), + input_select_word_forward: z + .string() + .optional() + .default("alt+shift+f,alt+shift+right") + .describe("Select word forward in input"), + input_select_word_backward: z + .string() + .optional() + .default("alt+shift+b,alt+shift+left") + .describe("Select word backward in input"), + input_delete_word_forward: z + .string() + .optional() + .default("alt+d,alt+delete,ctrl+delete") + .describe("Delete word forward in input"), + input_delete_word_backward: z + .string() + .optional() + .default("ctrl+w,ctrl+backspace,alt+backspace") + .describe("Delete word backward in input"), + history_previous: z.string().optional().default("up").describe("Previous history item"), + history_next: z.string().optional().default("down").describe("Next history item"), + session_child_first: z.string().optional().default("down").describe("Go to first child session"), + session_child_cycle: z.string().optional().default("right").describe("Go to next child session"), + session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"), + session_parent: z.string().optional().default("up").describe("Go to parent session"), + terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"), + terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"), + tips_toggle: z.string().optional().default("h").describe("Toggle tips on home screen"), + plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"), + display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"), + }) + .strict() + .meta({ + ref: "KeybindsConfig", + }) + + export const Server = z + .object({ + port: z.number().int().positive().optional().describe("Port to listen on"), + hostname: z.string().optional().describe("Hostname to listen on"), + mdns: z.boolean().optional().describe("Enable mDNS service discovery"), + mdnsDomain: z.string().optional().describe("Custom domain name for mDNS service (default: opencode.local)"), + cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"), + }) + .strict() + .meta({ + ref: "ServerConfig", + }) + + export const Layout = z.enum(["auto", "stretch"]).meta({ + ref: "LayoutConfig", + }) + export type Layout = z.infer + + export const Model = z + .object({ + id: z.string(), + name: z.string(), + family: z.string().optional(), + release_date: z.string(), + attachment: z.boolean(), + reasoning: z.boolean(), + temperature: z.boolean(), + tool_call: z.boolean(), + interleaved: z + .union([ + z.literal(true), +======= + .describe("URLs to fetch skills from (e.g., https://example.com/.well-known/skills/)"), + }) + export type Skills = z.infer + + export const Agent = z + .object({ + model: ModelId.optional(), + variant: z + .string() + .optional() + .describe("Default model variant for this agent (applies only when using the agent's configured model)."), + temperature: z.number().optional(), + top_p: z.number().optional(), + prompt: z.string().optional(), + tools: z.record(z.string(), z.boolean()).optional().describe("@deprecated Use 'permission' field instead"), + disable: z.boolean().optional(), + description: z.string().optional().describe("Description of when to use the agent"), + mode: z.enum(["subagent", "primary", "all"]).optional(), + hidden: z + .boolean() + .optional() + .describe("Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)"), + options: z.record(z.string(), z.any()).optional(), + color: z + .union([ + z.string().regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format"), + z.enum(["primary", "secondary", "accent", "success", "warning", "error", "info"]), + ]) + .optional() + .describe("Hex color code (e.g., #FF5733) or theme color (e.g., primary)"), + steps: z + .number() + .int() + .positive() + .optional() + .describe("Maximum number of agentic iterations before forcing text-only response"), + maxSteps: z.number().int().positive().optional().describe("@deprecated Use 'steps' field instead."), + permission: Permission.optional(), + }) + .catchall(z.any()) + .transform((agent, ctx) => { + const knownKeys = new Set([ + "name", + "model", + "variant", + "prompt", + "description", + "temperature", + "top_p", + "mode", + "hidden", + "color", + "steps", + "maxSteps", + "options", + "permission", + "disable", + "tools", + ]) + + // Extract unknown properties into options + const options: Record = { ...agent.options } + for (const [key, value] of Object.entries(agent)) { + if (!knownKeys.has(key)) options[key] = value + } + + // Convert legacy tools config to permissions + const permission: Permission = {} + for (const [tool, enabled] of Object.entries(agent.tools ?? {})) { + const action = enabled ? "allow" : "deny" + // write, edit, patch, multiedit all map to edit permission + if (tool === "write" || tool === "edit" || tool === "patch" || tool === "multiedit") { + permission.edit = action + } else { + permission[tool] = action + } + } + Object.assign(permission, agent.permission) + + // Convert legacy maxSteps to steps + const steps = agent.steps ?? agent.maxSteps + + return { ...agent, options, permission, steps } as typeof agent & { + options?: Record + permission?: Permission + steps?: number + } + }) + .meta({ + ref: "AgentConfig", + }) + export type Agent = z.infer + + export const Keybinds = z + .object({ + leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"), + app_exit: z.string().optional().default("ctrl+c,ctrl+d,q").describe("Exit the application"), + editor_open: z.string().optional().default("e").describe("Open external editor"), + theme_list: z.string().optional().default("t").describe("List available themes"), + sidebar_toggle: z.string().optional().default("b").describe("Toggle sidebar"), + scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"), + username_toggle: z.string().optional().default("none").describe("Toggle username visibility"), + status_view: z.string().optional().default("s").describe("View status"), + session_export: z.string().optional().default("x").describe("Export session to editor"), + session_new: z.string().optional().default("n").describe("Create a new session"), + session_list: z.string().optional().default("l").describe("List all sessions"), + session_timeline: z.string().optional().default("g").describe("Show session timeline"), + session_fork: z.string().optional().default("none").describe("Fork session from message"), + session_rename: z.string().optional().default("ctrl+r").describe("Rename session"), + session_delete: z.string().optional().default("ctrl+d").describe("Delete session"), + 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"), + 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"), + session_compact: z.string().optional().default("c").describe("Compact the session"), + messages_page_up: z.string().optional().default("pageup,ctrl+alt+b").describe("Scroll messages up by one page"), + messages_page_down: z + .string() + .optional() + .default("pagedown,ctrl+alt+f") + .describe("Scroll messages down by one page"), + messages_line_up: z.string().optional().default("ctrl+alt+y").describe("Scroll messages up by one line"), + messages_line_down: z.string().optional().default("ctrl+alt+e").describe("Scroll messages down by one line"), + messages_half_page_up: z.string().optional().default("ctrl+alt+u").describe("Scroll messages up by half page"), + messages_half_page_down: z + .string() + .optional() + .default("ctrl+alt+d") + .describe("Scroll messages down by half page"), + messages_first: z.string().optional().default("ctrl+g,home").describe("Navigate to first message"), + messages_last: z.string().optional().default("ctrl+alt+g,end").describe("Navigate to last message"), + messages_next: z.string().optional().default("none").describe("Navigate to next message"), + messages_previous: z.string().optional().default("none").describe("Navigate to previous message"), + messages_last_user: z.string().optional().default("none").describe("Navigate to last user message"), + messages_copy: z.string().optional().default("y").describe("Copy message"), + messages_undo: z.string().optional().default("u").describe("Undo message"), + messages_redo: z.string().optional().default("r").describe("Redo message"), + messages_toggle_conceal: z + .string() + .optional() + .default("h") + .describe("Toggle code block concealment in messages"), + tool_details: z.string().optional().default("none").describe("Toggle tool details visibility"), + model_list: z.string().optional().default("m").describe("List available models"), + model_cycle_recent: z.string().optional().default("f2").describe("Next recently used model"), + model_cycle_recent_reverse: z.string().optional().default("shift+f2").describe("Previous recently used model"), + model_cycle_favorite: z.string().optional().default("none").describe("Next favorite model"), + model_cycle_favorite_reverse: z.string().optional().default("none").describe("Previous favorite model"), + command_list: z.string().optional().default("ctrl+p").describe("List available commands"), + agent_list: z.string().optional().default("a").describe("List agents"), + prompt_skills: z.string().optional().default("k").describe("Open skills search"), + agent_cycle: z.string().optional().default("tab").describe("Next agent"), + agent_cycle_reverse: z.string().optional().default("shift+tab").describe("Previous agent"), + variant_cycle: z.string().optional().default("ctrl+t").describe("Cycle model variants"), + variant_list: z.string().optional().default("none").describe("List 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"), + input_submit: z.string().optional().default("return").describe("Submit input"), + input_newline: z + .string() + .optional() + .default("shift+return,ctrl+return,alt+return,ctrl+j") + .describe("Insert newline in input"), + input_move_left: z.string().optional().default("left,ctrl+b").describe("Move cursor left in input"), + input_move_right: z.string().optional().default("right,ctrl+f").describe("Move cursor right in input"), + input_move_up: z.string().optional().default("up").describe("Move cursor up in input"), + input_move_down: z.string().optional().default("down").describe("Move cursor down in input"), + input_select_left: z.string().optional().default("shift+left").describe("Select left in input"), + input_select_right: z.string().optional().default("shift+right").describe("Select right in input"), + input_select_up: z.string().optional().default("shift+up").describe("Select up in input"), + input_select_down: z.string().optional().default("shift+down").describe("Select down in input"), + input_line_home: z.string().optional().default("ctrl+a").describe("Move to start of line in input"), + input_line_end: z.string().optional().default("ctrl+e").describe("Move to end of line in input"), + input_select_line_home: z + .string() + .optional() + .default("ctrl+shift+a") + .describe("Select to start of line in input"), + input_select_line_end: z.string().optional().default("ctrl+shift+e").describe("Select to end of line in input"), + input_visual_line_home: z.string().optional().default("alt+a").describe("Move to start of visual line in input"), + input_visual_line_end: z.string().optional().default("alt+e").describe("Move to end of visual line in input"), + input_select_visual_line_home: z + .string() + .optional() + .default("alt+shift+a") + .describe("Select to start of visual line in input"), + input_select_visual_line_end: z + .string() + .optional() + .default("alt+shift+e") + .describe("Select to end of visual line in input"), + input_buffer_home: z.string().optional().default("home").describe("Move to start of buffer in input"), + input_buffer_end: z.string().optional().default("end").describe("Move to end of buffer in input"), + input_select_buffer_home: z + .string() + .optional() + .default("shift+home") + .describe("Select to start of buffer in input"), + input_select_buffer_end: z.string().optional().default("shift+end").describe("Select to end of buffer in input"), + input_delete_line: z.string().optional().default("ctrl+shift+d").describe("Delete line in input"), + input_delete_to_line_end: z.string().optional().default("ctrl+k").describe("Delete to end of line in input"), + input_delete_to_line_start: z.string().optional().default("ctrl+u").describe("Delete to start of line in input"), + input_backspace: z.string().optional().default("backspace,shift+backspace").describe("Backspace in input"), + input_delete: z.string().optional().default("ctrl+d,delete,shift+delete").describe("Delete character in input"), + input_undo: z.string().optional().default("ctrl+-,super+z").describe("Undo in input"), + input_redo: z.string().optional().default("ctrl+.,super+shift+z").describe("Redo in input"), + input_word_forward: z + .string() + .optional() + .default("alt+f,alt+right,ctrl+right") + .describe("Move word forward in input"), + input_word_backward: z + .string() + .optional() + .default("alt+b,alt+left,ctrl+left") + .describe("Move word backward in input"), + input_select_word_forward: z + .string() + .optional() + .default("alt+shift+f,alt+shift+right") + .describe("Select word forward in input"), + input_select_word_backward: z + .string() + .optional() + .default("alt+shift+b,alt+shift+left") + .describe("Select word backward in input"), + input_delete_word_forward: z + .string() + .optional() + .default("alt+d,alt+delete,ctrl+delete") + .describe("Delete word forward in input"), + input_delete_word_backward: z + .string() + .optional() + .default("ctrl+w,ctrl+backspace,alt+backspace") + .describe("Delete word backward in input"), + history_previous: z.string().optional().default("up").describe("Previous history item"), + history_next: z.string().optional().default("down").describe("Next history item"), + session_child_first: z.string().optional().default("down").describe("Go to first child session"), + session_child_cycle: z.string().optional().default("right").describe("Go to next child session"), + session_child_cycle_reverse: z.string().optional().default("left").describe("Go to previous child session"), + session_parent: z.string().optional().default("up").describe("Go to parent session"), + terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"), + terminal_title_toggle: z.string().optional().default("none").describe("Toggle terminal title"), + tips_toggle: z.string().optional().default("h").describe("Toggle tips on home screen"), + plugin_manager: z.string().optional().default("none").describe("Open plugin manager dialog"), + display_thinking: z.string().optional().default("none").describe("Toggle thinking blocks visibility"), + }) + .strict() + .meta({ + ref: "KeybindsConfig", + }) + + export const Server = z + .object({ + port: z.number().int().positive().optional().describe("Port to listen on"), + hostname: z.string().optional().describe("Hostname to listen on"), + mdns: z.boolean().optional().describe("Enable mDNS service discovery"), + mdnsDomain: z.string().optional().describe("Custom domain name for mDNS service (default: opencode.local)"), + cors: z.array(z.string()).optional().describe("Additional domains to allow for CORS"), + }) + .strict() + .meta({ + ref: "ServerConfig", + }) + + export const Layout = z.enum(["auto", "stretch"]).meta({ + ref: "LayoutConfig", + }) + export type Layout = z.infer + + export const Model = z + .object({ + id: z.string(), + name: z.string(), + family: z.string().optional(), + release_date: z.string(), + attachment: z.boolean(), + reasoning: z.boolean(), + temperature: z.boolean(), + tool_call: z.boolean(), + interleaved: z + .union([ + z.literal(true), z .object({ enabled: z.boolean(), diff --git a/packages/web/src/content/docs/keybinds.mdx b/packages/web/src/content/docs/keybinds.mdx index 5488aaf81cbe..1c2da5fc5a30 100644 --- a/packages/web/src/content/docs/keybinds.mdx +++ b/packages/web/src/content/docs/keybinds.mdx @@ -56,6 +56,7 @@ OpenCode has a list of keybinds that you can customize through `tui.json`. "variant_list": "none", "command_list": "ctrl+p", "agent_list": "a", + "prompt_skills": "k", "agent_cycle": "tab", "agent_cycle_reverse": "shift+tab", "input_clear": "ctrl+c", From 9486d06002ea68cf10380b31cce56f12c54a7300 Mon Sep 17 00:00:00 2001 From: Raine Virta Date: Mon, 13 Apr 2026 22:43:32 +0300 Subject: [PATCH 3/4] remove explicit no-commit prompt rule --- packages/opencode/src/session/prompt/default.txt | 2 -- packages/opencode/src/session/prompt/trinity.txt | 2 -- packages/opencode/src/tool/bash.txt | 3 +-- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/opencode/src/session/prompt/default.txt b/packages/opencode/src/session/prompt/default.txt index 365663eeef62..2095a42da37d 100644 --- a/packages/opencode/src/session/prompt/default.txt +++ b/packages/opencode/src/session/prompt/default.txt @@ -83,8 +83,6 @@ The user will primarily request you perform software engineering tasks. This inc - Implement the solution using all tools available to you - Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach. - VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (e.g. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time. -NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. - - Tool results and user messages may include tags. tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result. # Tool usage policy diff --git a/packages/opencode/src/session/prompt/trinity.txt b/packages/opencode/src/session/prompt/trinity.txt index 28ee4c4f2692..cb60de268c06 100644 --- a/packages/opencode/src/session/prompt/trinity.txt +++ b/packages/opencode/src/session/prompt/trinity.txt @@ -75,8 +75,6 @@ The user will primarily request you perform software engineering tasks. This inc - Implement the solution using all tools available to you - Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach. - VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (e.g. npm run lint, npm run typecheck, ruff, etc.) with Bash if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time. -NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. - - Tool results and user messages may include tags. tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result. # Tool usage policy diff --git a/packages/opencode/src/tool/bash.txt b/packages/opencode/src/tool/bash.txt index 668cea307ce4..dda1c1686108 100644 --- a/packages/opencode/src/tool/bash.txt +++ b/packages/opencode/src/tool/bash.txt @@ -50,7 +50,7 @@ Usage notes: # Committing changes with git -Only create commits when requested by the user. If unclear, ask first. When the user asks you to create a new git commit, follow these steps carefully: +When the user asks you to create a new git commit, follow these steps carefully: Git Safety Protocol: - NEVER update the git config @@ -63,7 +63,6 @@ Git Safety Protocol: (3) Commit has NOT been pushed to remote (verify: git status shows "Your branch is ahead") - CRITICAL: If commit FAILED or was REJECTED by hook, NEVER amend - fix the issue and create a NEW commit - CRITICAL: If you already pushed to remote, NEVER amend unless user explicitly requests it (requires force push) -- NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. 1. You can call multiple tools in a single response. When multiple independent pieces of information are requested and all commands are likely to succeed, run multiple tool calls in parallel for optimal performance. run the following bash commands in parallel, each using the Bash tool: - Run a git status command to see all untracked files. From 84537a4542f5d9ba00ef37fcfebccf88ccc9ec0f Mon Sep 17 00:00:00 2001 From: Raine Virta Date: Mon, 13 Apr 2026 22:51:07 +0300 Subject: [PATCH 4/4] improve local install flow Combine the local install script and follow-up fixes so launcher, bootstrap, and nodenv handling ship as one coherent change. --- packages/opencode/script/install-bin | 58 ++++++++++++++++++++++++++++ raine/README.md | 19 +++++++++ raine/install-local.sh | 7 ++++ 3 files changed, 84 insertions(+) create mode 100755 packages/opencode/script/install-bin create mode 100644 raine/README.md create mode 100755 raine/install-local.sh diff --git a/packages/opencode/script/install-bin b/packages/opencode/script/install-bin new file mode 100755 index 000000000000..3b143ead94f1 --- /dev/null +++ b/packages/opencode/script/install-bin @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +os="$(uname -s | tr '[:upper:]' '[:lower:]')" +arch="$(uname -m)" +dir="${XDG_BIN_DIR:-$HOME/.local/bin}" +name="opencode" +build=0 + +case "$arch" in + x86_64) arch="x64" ;; + aarch64) arch="arm64" ;; +esac + +while [[ $# -gt 0 ]]; do + case "$1" in + --build) + build=1 + shift + ;; + --dir) + dir="$2" + shift 2 + ;; + --name) + name="$2" + shift 2 + ;; + *) + echo "unknown argument: $1" >&2 + echo "usage: ./script/install-bin [--build] [--dir ] [--name ]" >&2 + exit 1 + ;; + esac +done + +bin="$root/dist/opencode-$os-$arch/bin/opencode" +target="$dir/$name" + +if [[ $build -eq 1 || ! -x "$bin" ]]; then + bun "$root/script/build.ts" --single +fi + +if [[ ! -x "$bin" ]]; then + echo "missing local binary: $bin" >&2 + exit 1 +fi + +mkdir -p "$dir" +ln -sf "$bin" "$target" + +echo "linked $target -> $bin" + +case ":${PATH:-}:" in + *":$dir:"*) ;; + *) echo "warning: $dir is not on PATH" >&2 ;; +esac diff --git a/raine/README.md b/raine/README.md new file mode 100644 index 000000000000..fc2d716afb65 --- /dev/null +++ b/raine/README.md @@ -0,0 +1,19 @@ +# Raine Local Install + +Build and link the local `opencode` binary from this checkout: + +```bash +./raine/install-local.sh +``` + +The script runs `packages/opencode/script/install-bin --build`, which: + +- builds the standalone binary when needed +- links it to `~/.local/bin/opencode` by default + +Verify the install: + +```bash +which opencode +opencode --version +``` diff --git a/raine/install-local.sh b/raine/install-local.sh new file mode 100755 index 000000000000..537a40e6083c --- /dev/null +++ b/raine/install-local.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" + +cd "$root/packages/opencode" +./script/install-bin --build "$@"