summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-07-09 15:44:59 -0400
committerDax Raad <[email protected]>2025-07-09 21:59:38 -0400
commita826936702251df6a88d90f32f8570e68a4e7995 (patch)
tree7a3ef69e4f9088d03f540673799773a16d95f3b9
parentfd4a5d5a63fc6079612460c4c8750f02f9983842 (diff)
downloadopencode-a826936702251df6a88d90f32f8570e68a4e7995.tar.gz
opencode-a826936702251df6a88d90f32f8570e68a4e7995.zip
modes concept
-rw-r--r--packages/opencode/src/config/config.ts19
-rw-r--r--packages/opencode/src/server/server.ts21
-rw-r--r--packages/opencode/src/session/index.ts27
-rw-r--r--packages/opencode/src/session/message-v2.ts1
-rw-r--r--packages/opencode/src/session/mode.ts74
-rw-r--r--packages/opencode/src/session/prompt/plan.txt3
6 files changed, 140 insertions, 5 deletions
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index 7c248da83..9d6ca2ca4 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -55,6 +55,17 @@ export namespace Config {
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
export type Mcp = z.infer<typeof Mcp>
+ export const Mode = z
+ .object({
+ model: z.string().optional(),
+ prompt: z.string().optional(),
+ tools: z.record(z.string(), z.boolean()).optional(),
+ })
+ .openapi({
+ ref: "ModeConfig",
+ })
+ export type Mode = z.infer<typeof Mode>
+
export const Keybinds = z
.object({
leader: z.string().optional().default("ctrl+x").describe("Leader key for keybind combinations"),
@@ -99,6 +110,7 @@ export namespace Config {
.openapi({
ref: "KeybindsConfig",
})
+
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
@@ -108,6 +120,13 @@ export namespace Config {
autoupdate: z.boolean().optional().describe("Automatically update to the latest version"),
disabled_providers: z.array(z.string()).optional().describe("Disable providers that are loaded automatically"),
model: z.string().describe("Model to use in the format of provider/model, eg anthropic/claude-2").optional(),
+ mode: z
+ .object({
+ build: Mode.optional(),
+ plan: Mode.optional(),
+ })
+ .catchall(Mode)
+ .optional(),
log_level: Log.Level.optional().describe("Minimum log level to write to log files"),
provider: z
.record(
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 38a808974..db6a8fdf6 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -16,6 +16,7 @@ import { Config } from "../config/config"
import { File } from "../file"
import { LSP } from "../lsp"
import { MessageV2 } from "../session/message-v2"
+import { Mode } from "../session/mode"
const ERRORS = {
400: {
@@ -681,6 +682,26 @@ export namespace Server {
return c.json(true)
},
)
+ .get(
+ "/mode",
+ describeRoute({
+ description: "List all modes",
+ responses: {
+ 200: {
+ description: "List of modes",
+ content: {
+ "application/json": {
+ schema: resolver(Mode.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const modes = await Mode.list()
+ return c.json(modes)
+ },
+ )
return result
}
diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts
index a1a1d183a..0e1861f28 100644
--- a/packages/opencode/src/session/index.ts
+++ b/packages/opencode/src/session/index.ts
@@ -15,6 +15,7 @@ import {
} from "ai"
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
+import PROMPT_PLAN from "../session/prompt/plan.txt"
import { App } from "../app/app"
import { Bus } from "../bus"
@@ -29,12 +30,12 @@ import type { ModelsDev } from "../provider/models"
import { Share } from "../share/share"
import { Snapshot } from "../snapshot"
import { Storage } from "../storage/storage"
-import type { Tool } from "../tool/tool"
import { Log } from "../util/log"
import { NamedError } from "../util/error"
import { SystemPrompt } from "./system"
import { FileTime } from "../file/time"
import { MessageV2 } from "./message-v2"
+import { Mode } from "./mode"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -281,13 +282,13 @@ export namespace Session {
sessionID: string
providerID: string
modelID: string
+ mode?: string
parts: MessageV2.UserPart[]
- system?: string[]
- tools?: Tool.Info[]
}) {
using abort = lock(input.sessionID)
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
+
const model = await Provider.getModel(input.providerID, input.modelID)
let msgs = await messages(input.sessionID)
const session = await get(input.sessionID)
@@ -364,6 +365,7 @@ export namespace Session {
return [
{
type: "text",
+ synthetic: true,
text: ["Called the Read tool on " + url.pathname, "<results>", text, "</results>"].join("\n"),
},
]
@@ -373,6 +375,7 @@ export namespace Session {
{
type: "text",
text: `Called the Read tool with the following input: {\"filePath\":\"${url.pathname}\"}`,
+ synthetic: true,
},
{
type: "file",
@@ -386,6 +389,14 @@ export namespace Session {
return [part]
}),
).then((x) => x.flat())
+
+ if (true)
+ input.parts.push({
+ type: "text",
+ text: PROMPT_PLAN,
+ synthetic: true,
+ })
+
if (msgs.length === 0 && !session.parentID) {
generateText({
maxOutputTokens: input.providerID === "google" ? 1024 : 20,
@@ -431,9 +442,13 @@ export namespace Session {
await updateMessage(msg)
msgs.push(msg)
- const system = input.system ?? SystemPrompt.provider(input.providerID, input.modelID)
+ const mode = await Mode.get(input.mode ?? "build")
+ let system = mode.prompt ? [mode.prompt] : SystemPrompt.provider(input.providerID, input.modelID)
system.push(...(await SystemPrompt.environment()))
system.push(...(await SystemPrompt.custom()))
+ // max 2 system prompt messages for caching purposes
+ const [first, ...rest] = system
+ system = [first, rest.join("\n")]
const next: MessageV2.Info = {
id: Identifier.ascending("message"),
@@ -462,7 +477,8 @@ export namespace Session {
const tools: Record<string, AITool> = {}
for (const item of await Provider.tools(input.providerID)) {
- tools[item.id.replaceAll(".", "_")] = tool({
+ if (mode.tools[item.id] === false) continue
+ tools[item.id] = tool({
id: item.id as any,
description: item.description,
inputSchema: item.parameters as ZodSchema,
@@ -494,6 +510,7 @@ export namespace Session {
}
for (const [key, item] of Object.entries(await MCP.tools())) {
+ if (mode.tools[key] === false) continue
const execute = item.execute
if (!execute) continue
item.execute = async (args, opts) => {
diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts
index 8b09e68e6..fba34f4c0 100644
--- a/packages/opencode/src/session/message-v2.ts
+++ b/packages/opencode/src/session/message-v2.ts
@@ -76,6 +76,7 @@ export namespace MessageV2 {
.object({
type: z.literal("text"),
text: z.string(),
+ synthetic: z.boolean().optional(),
})
.openapi({
ref: "TextPart",
diff --git a/packages/opencode/src/session/mode.ts b/packages/opencode/src/session/mode.ts
new file mode 100644
index 000000000..d2f548580
--- /dev/null
+++ b/packages/opencode/src/session/mode.ts
@@ -0,0 +1,74 @@
+import { mergeDeep } from "remeda"
+import { App } from "../app/app"
+import { Config } from "../config/config"
+import z from "zod"
+
+export namespace Mode {
+ export const Info = z
+ .object({
+ name: z.string(),
+ model: z
+ .object({
+ modelID: z.string(),
+ providerID: z.string(),
+ })
+ .optional(),
+ prompt: z.string().optional(),
+ tools: z
+ .object({
+ write: z.boolean().optional(),
+ edit: z.boolean().optional(),
+ patch: z.boolean().optional(),
+ })
+ .optional(),
+ })
+ .openapi({
+ ref: "Mode",
+ })
+ export type Info = z.infer<typeof Info>
+ const state = App.state("mode", async () => {
+ const cfg = await Config.get()
+ const mode = mergeDeep(
+ {
+ build: {},
+ plan: {
+ tools: {
+ write: false,
+ edit: false,
+ patch: false,
+ },
+ },
+ },
+ cfg.mode ?? {},
+ )
+ const result: Record<string, Info> = {}
+ for (const [key, value] of Object.entries(mode)) {
+ let item = result[key]
+ if (!item)
+ item = result[key] = {
+ name: key,
+ tools: {},
+ }
+ const model = value.model ?? cfg.model
+ if (model) {
+ const [providerID, ...rest] = model.split("/")
+ const modelID = rest.join("/")
+ item.model = {
+ modelID,
+ providerID,
+ }
+ }
+ if (value.prompt) item.prompt = await Bun.file(value.prompt).text()
+ if (value.tools) item.tools = value.tools
+ }
+ return result
+ })
+
+ export async function get(mode: string) {
+ return state().then((x) => x[mode])
+ }
+
+ export async function list() {
+ return state().then((x) => Object.values(x))
+ }
+}
diff --git a/packages/opencode/src/session/prompt/plan.txt b/packages/opencode/src/session/prompt/plan.txt
new file mode 100644
index 000000000..fffbfffc0
--- /dev/null
+++ b/packages/opencode/src/session/prompt/plan.txt
@@ -0,0 +1,3 @@
+<system-reminder>
+Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supercedes any other instructions you have received (for example, to make edits).
+</system-reminder>