From f5d17c34f036026a67c8ea0dc559c9c1e1c92a31 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 17:31:38 +0000 Subject: [PATCH 01/27] feat: implement hot-reload for agents, skills, and config - Added OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC and OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG flags - Extended FileWatcher to support watching config directories with context preservation - Implemented initWatcher logic for Config, Skill, Agent, Command, and ToolRegistry - Added TUI and App event handlers for live updates - Enhanced instance context management for async watcher callbacks - Added tests for config and skill hot reloading --- packages/app/src/context/global-sync.tsx | 56 +- packages/opencode/src/agent/agent.ts | 20 + packages/opencode/src/bus/index.ts | 2 +- .../src/cli/cmd/tui/context/local.tsx | 9 +- .../opencode/src/cli/cmd/tui/context/sync.tsx | 15 +- packages/opencode/src/cli/cmd/tui/thread.ts | 1 + packages/opencode/src/command/index.ts | 33 +- packages/opencode/src/config/config.ts | 43 +- packages/opencode/src/config/markdown.ts | 4 +- packages/opencode/src/file/watcher.ts | 95 +- packages/opencode/src/flag/flag.ts | 4 + packages/opencode/src/project/bootstrap.ts | 14 + packages/opencode/src/project/instance.ts | 22 + packages/opencode/src/project/state.ts | 10 + packages/opencode/src/skill/skill.ts | 34 + packages/opencode/src/tool/registry.ts | 30 +- .../config/config-delete-hot-reload.test.ts | 117 + packages/opencode/test/fixture/fixture.ts | 2 + .../test/skill/skill-hot-reload.test.ts | 457 +++ packages/sdk/js/src/v2/gen/types.gen.ts | 2674 +++++++++-------- 20 files changed, 2281 insertions(+), 1361 deletions(-) create mode 100644 packages/opencode/test/config/config-delete-hot-reload.test.ts create mode 100644 packages/opencode/test/skill/skill-hot-reload.test.ts diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index a0b25705686..7f55912cd09 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -117,6 +117,15 @@ function createGlobalSync() { return children[directory] } + function createClient(directory: string) { + return createOpencodeClient({ + baseUrl: globalSDK.url, + fetch: platform.fetch, + directory, + throwOnError: true, + }) + } + async function loadSessions(directory: string) { const [store, setStore] = child(directory) const limit = store.limit @@ -157,12 +166,7 @@ function createGlobalSync() { async function bootstrapInstance(directory: string) { if (!directory) return const [store, setStore] = child(directory) - const sdk = createOpencodeClient({ - baseUrl: globalSDK.url, - fetch: platform.fetch, - directory, - throwOnError: true, - }) + const sdk = createClient(directory) const blockingRequests = { project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)), @@ -267,7 +271,7 @@ function createGlobalSync() { const unsub = globalSDK.event.listen((e) => { const directory = e.name - const event = e.details + const event = e.details as any if (directory === "global") { switch (event?.type) { @@ -485,15 +489,41 @@ function createGlobalSync() { break } case "lsp.updated": { - const sdk = createOpencodeClient({ - baseUrl: globalSDK.url, - fetch: platform.fetch, - directory, - throwOnError: true, - }) + const sdk = createClient(directory) sdk.lsp.status().then((x) => setStore("lsp", x.data ?? [])) break } + case "config.updated": { + const sdk = createClient(directory) + Promise.all([ + sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), + sdk.command.list().then((x) => setStore("command", x.data ?? [])), + sdk.config.get().then((x) => setStore("config", x.data!)), + ]).catch((err) => { + console.error("Failed to refresh config-related data:", err) + showToast({ + title: "Config Refresh Failed", + description: "Some updates may not be reflected. Try reloading.", + variant: "error", + }) + }) + break + } + case "skill.updated": { + const sdk = createClient(directory) + sdk.app + .agents() + .then((x) => setStore("agent", x.data ?? [])) + .catch((err) => { + console.error("Failed to reload agents after skill update:", err) + showToast({ + title: "Agent Refresh Failed", + description: "Could not update agents after a skill change.", + variant: "error", + }) + }) + break + } } }) onCleanup(unsub) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 64875091916..c457b80ec2a 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -5,6 +5,9 @@ import { generateObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" import { Truncate } from "../tool/truncation" +import { Bus } from "@/bus" +import { Flag } from "@/flag/flag" +import { Log } from "@/util/log" import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" @@ -17,6 +20,8 @@ import { Global } from "@/global" import path from "path" export namespace Agent { + const log = Log.create({ service: "agent" }) + export const Info = z .object({ name: z.string(), @@ -295,4 +300,19 @@ export namespace Agent { }) return result.object } + + /** + * Initialize agent hot-reload watcher. + * Agents are loaded from config, so we listen for config updates. + * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC flag. + */ + export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + + Bus.subscribe(Config.Events.Updated, async () => { + log.info("config updated, agents will be refreshed on next access") + // Agents are loaded lazily from Config, so nothing to do here + // The next call to Agent.get() or Agent.list() will get fresh data + }) + } } diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index edb093f1974..1021c8683b2 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -56,11 +56,11 @@ export namespace Bus { pending.push(sub(payload)) } } + await Promise.all(pending) GlobalBus.emit("event", { directory: Instance.directory, payload, }) - return Promise.all(pending) } export function subscribe( diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 63f1d9743bf..c64f2d13436 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -54,7 +54,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ return agents() }, current() { - return agents().find((x) => x.name === agentStore.current)! + const match = agents().find((x) => x.name === agentStore.current) + if (match) return match + // Fallback if current agent was removed (e.g. via hot reload) + if (agents().length > 0) { + setAgentStore("current", agents()[0].name) + return agents()[0] + } + return agents()[0]! }, set(name: string) { if (!agents().some((x) => x.name === name)) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 0edc911344c..211749aabd2 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -105,7 +105,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const sdk = useSDK() sdk.event.listen((e) => { - const event = e.details + const event = e.details as any switch (event.type) { case "server.instance.disposed": bootstrap() @@ -304,6 +304,19 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ setStore("vcs", { branch: event.properties.branch }) break } + + case "config.updated": { + // @ts-ignore - type will be generated + setStore("config", reconcile(event.properties)) + sdk.client.app.agents().then((x) => setStore("agent", reconcile(x.data ?? []))) + sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))) + break + } + + case "skill.updated": { + sdk.client.app.agents().then((x) => setStore("agent", reconcile(x.data ?? []))) + break + } } }) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 05714268545..8aede78c8df 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -144,6 +144,7 @@ export const TuiThreadCommand = cmd({ url, fetch: customFetch, events, + directory: cwd, args: { continue: args.continue, sessionID: args.session, diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 976f1cd51e9..336480b92b3 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -1,4 +1,5 @@ import { BusEvent } from "@/bus/bus-event" +import { Bus } from "@/bus" import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" @@ -6,9 +7,14 @@ import { Identifier } from "../id/id" import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" import { MCP } from "../mcp" +import { Flag } from "@/flag/flag" +import { Log } from "@/util/log" export namespace Command { + const log = Log.create({ service: "command" }) + export const Event = { + Updated: BusEvent.define("command.updated", z.object({})), Executed: BusEvent.define( "command.executed", z.object({ @@ -55,7 +61,7 @@ export namespace Command { REVIEW: "review", } as const - const state = Instance.state(async () => { + const initState = async () => { const cfg = await Config.get() const result: Record = { @@ -119,7 +125,9 @@ export namespace Command { } return result - }) + } + + const state = Instance.state(initState) export async function get(name: string) { return state().then((x) => x[name]) @@ -128,4 +136,25 @@ export namespace Command { export async function list() { return state().then((x) => Object.values(x)) } + + /** + * Initialize command hot-reload watcher. + * Commands are loaded from config, so we listen for config updates. + * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC flag. + */ + export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + + Bus.subscribe(Config.Events.Updated, async () => { + log.info("config updated, reloading commands") + try { + await Instance.invalidate(initState) + await list() // Force reload + Bus.publish(Event.Updated, {}) + log.info("commands reloaded successfully") + } catch (err) { + log.error("failed to reload commands", { error: err }) + } + }) + } } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 06803879f38..8dc7057f22c 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -19,10 +19,20 @@ import { BunProc } from "@/bun" import { Installation } from "@/installation" import { ConfigMarkdown } from "./markdown" import { existsSync } from "fs" +import { BusEvent } from "@/bus/bus-event" +import { Bus } from "@/bus" +import { FileWatcher } from "@/file/watcher" export namespace Config { const log = Log.create({ service: "config" }) + export const Events = { + Updated: BusEvent.define( + "config.updated", + z.lazy(() => Info), + ), + } + // Custom merge function that concatenates array fields instead of replacing them function mergeConfigConcatArrays(target: Info, source: Info): Info { const merged = mergeDeep(target, source) @@ -232,7 +242,7 @@ export namespace Config { cwd: dir, })) { const md = await ConfigMarkdown.parse(item) - if (!md.data) continue + if (!md?.data) continue const patterns = ["/.opencode/command/", "/.opencode/commands/", "/command/", "/commands/"] const file = rel(item, patterns) ?? path.basename(item) @@ -264,7 +274,7 @@ export namespace Config { cwd: dir, })) { const md = await ConfigMarkdown.parse(item) - if (!md.data) continue + if (!md?.data) continue const patterns = ["/.opencode/agent/", "/.opencode/agents/", "/agent/", "/agents/"] const file = rel(item, patterns) ?? path.basename(item) @@ -295,7 +305,7 @@ export namespace Config { cwd: dir, })) { const md = await ConfigMarkdown.parse(item) - if (!md.data) continue + if (!md?.data) continue const config = { name: path.basename(item, ".md"), @@ -1218,4 +1228,31 @@ export namespace Config { export async function directories() { return state().then((x) => x.directories) } + + /** + * Initialize config hot-reload watcher. + * Listens for file changes in config directories and reloads config when opencode.json changes. + * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG flag. + */ + export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) return + + const configPatterns = ["opencode.json", "opencode.jsonc"] + + Bus.subscribe(FileWatcher.Event.Updated, async (event) => { + const filename = path.basename(event.properties.file) + if (!configPatterns.includes(filename)) return + + log.info("config file changed, reloading", { file: event.properties.file, event: event.properties.event }) + + try { + // Invalidate and reload state + const result = await state() + Bus.publish(Events.Updated, result.config) + log.info("config reloaded successfully") + } catch (err) { + log.error("failed to reload config", { error: err }) + } + }) + } } diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index f20842c41a9..a1813a949f0 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -15,7 +15,9 @@ export namespace ConfigMarkdown { } export async function parse(filePath: string) { - const template = await Bun.file(filePath).text() + const file = Bun.file(filePath) + if (!(await file.exists())) return undefined + const template = await file.text() try { const md = matter(template) diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 44f8a0a3a4a..ef5bcdcc349 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -33,9 +33,29 @@ export namespace FileWatcher { } const watcher = lazy(() => { - const binding = require( - `@parcel/watcher-${process.platform}-${process.arch}${process.platform === "linux" ? `-${OPENCODE_LIBC || "glibc"}` : ""}`, - ) + const libc = typeof OPENCODE_LIBC === "string" && OPENCODE_LIBC.length > 0 ? OPENCODE_LIBC : "glibc" + + // Use static requires for bundler compatibility + let binding + if (process.platform === "darwin" && process.arch === "arm64") { + binding = require("@parcel/watcher-darwin-arm64") + } else if (process.platform === "darwin" && process.arch === "x64") { + binding = require("@parcel/watcher-darwin-x64") + } else if (process.platform === "linux" && process.arch === "arm64" && libc === "glibc") { + binding = require("@parcel/watcher-linux-arm64-glibc") + } else if (process.platform === "linux" && process.arch === "arm64" && libc === "musl") { + binding = require("@parcel/watcher-linux-arm64-musl") + } else if (process.platform === "linux" && process.arch === "x64" && libc === "glibc") { + binding = require("@parcel/watcher-linux-x64-glibc") + } else if (process.platform === "linux" && process.arch === "x64" && libc === "musl") { + binding = require("@parcel/watcher-linux-x64-musl") + } else if (process.platform === "win32" && process.arch === "x64") { + binding = require("@parcel/watcher-win32-x64") + } else { + const suffix = process.platform === "linux" ? `-${libc}` : "" + binding = require(`@parcel/watcher-${process.platform}-${process.arch}${suffix}`) + } + return createWrapper(binding) as typeof import("@parcel/watcher") }) @@ -54,18 +74,38 @@ export namespace FileWatcher { return {} } log.info("watcher backend", { platform: process.platform, backend }) + + // Capture directory now - callback runs outside AsyncLocalStorage context + const directory = Instance.directory const subscribe: ParcelWatcher.SubscribeCallback = (err, evts) => { if (err) return - for (const evt of evts) { - if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" }) - if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" }) - if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" }) - } + // Re-enter Instance context for Bus.publish to work correctly + Instance.runInContext(directory, () => { + for (const evt of evts) { + if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" }) + if (evt.type === "update") Bus.publish(Event.Updated, { file: evt.path, event: "change" }) + if (evt.type === "delete") Bus.publish(Event.Updated, { file: evt.path, event: "unlink" }) + } + }) } const subs: ParcelWatcher.AsyncSubscription[] = [] const cfgIgnores = cfg.watcher?.ignore ?? [] + const watchedPaths = new Set() + // Helper to check if path is already inside a watched directory + const isInsideWatchedPath = (targetPath: string) => { + const normalizedTarget = path.resolve(targetPath) + for (const watched of watchedPaths) { + const normalizedWatched = path.resolve(watched) + if (normalizedTarget === normalizedWatched || normalizedTarget.startsWith(normalizedWatched + path.sep)) { + return true + } + } + return false + } + + // 1. Watch Instance.directory (gated by upstream flag) if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) { const pending = watcher().subscribe(Instance.directory, subscribe, { ignore: [...FileIgnore.PATTERNS, ...cfgIgnores], @@ -76,9 +116,14 @@ export namespace FileWatcher { pending.then((s) => s.unsubscribe()).catch(() => {}) return undefined }) - if (sub) subs.push(sub) + if (sub) { + subs.push(sub) + watchedPaths.add(Instance.directory) + log.info("watching", { path: Instance.directory }) + } } + // 2. Watch .git directory for HEAD changes (always on for git projects) const vcsDir = await $`git rev-parse --git-dir` .quiet() .nothrow() @@ -98,7 +143,37 @@ export namespace FileWatcher { pending.then((s) => s.unsubscribe()).catch(() => {}) return undefined }) - if (sub) subs.push(sub) + if (sub) { + subs.push(sub) + watchedPaths.add(vcsDir) + log.info("watching", { path: vcsDir }) + } + } + + // 3. Watch config directories for hot-reload (gated by hot-reload flags) + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC() || Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) { + const configDirectories = await Config.directories() + for (const dir of configDirectories) { + if (isInsideWatchedPath(dir)) { + log.debug("skipping duplicate watch", { path: dir, reason: "already inside watched path" }) + continue + } + + const pending = watcher().subscribe(dir, subscribe, { + ignore: [...FileIgnore.PATTERNS], + backend, + }) + const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => { + log.error("failed to subscribe to config dir", { path: dir, error: err }) + pending.then((s) => s.unsubscribe()).catch(() => {}) + return undefined + }) + if (sub) { + subs.push(sub) + watchedPaths.add(dir) + log.info("watching", { path: dir }) + } + } } return { subs } diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 4cdb549096a..e1c420570d7 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -40,6 +40,10 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL") export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") + // Dynamic getters for hot reload to support testing overrides + export const OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC = () => truthy("OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC") + export const OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG = () => truthy("OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG") + function truthy(key: string) { const value = process.env[key]?.toLowerCase() return value === "true" || value === "1" diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 56fe4d13e66..6c009c754e9 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -11,6 +11,11 @@ import { Instance } from "./instance" import { Vcs } from "./vcs" import { Log } from "@/util/log" import { ShareNext } from "@/share/share-next" +import { Config } from "../config/config" +import { Skill } from "../skill/skill" +import { ToolRegistry } from "@/tool/registry" +import { Agent } from "@/agent/agent" +import { Flag } from "@/flag/flag" export async function InstanceBootstrap() { Log.Default.info("bootstrapping", { directory: Instance.directory }) @@ -20,6 +25,15 @@ export async function InstanceBootstrap() { Format.init() await LSP.init() FileWatcher.init() + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { + Skill.initWatcher() + Agent.initWatcher() + Command.initWatcher() + } + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) { + Config.initWatcher() + ToolRegistry.initWatcher() + } File.init() Vcs.init() diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index ddaa90f1e2b..e151864c789 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -6,6 +6,9 @@ import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" import { Filesystem } from "@/util/filesystem" +const MODULE_ID = Math.random() +console.log("DEBUG: Instance module loaded", MODULE_ID) + interface Context { directory: string worktree: string @@ -13,6 +16,8 @@ interface Context { } const context = Context.create("instance") const cache = new Map>() +// Resolved contexts for synchronous access (e.g., from native callbacks) +const resolved = new Map() export const Instance = { async provide(input: { directory: string; init?: () => Promise; fn: () => R }): Promise { @@ -29,6 +34,8 @@ export const Instance = { await context.provide(ctx, async () => { await input.init?.() }) + // Store resolved context for synchronous access + resolved.set(input.directory, ctx) return ctx }) cache.set(input.directory, existing) @@ -62,10 +69,14 @@ export const Instance = { state(init: () => S, dispose?: (state: Awaited) => Promise): () => S { return State.create(() => Instance.directory, init, dispose) }, + async invalidate(init: () => S) { + return State.invalidate(() => Instance.directory, init) + }, async dispose() { Log.Default.info("disposing instance", { directory: Instance.directory }) await State.dispose(Instance.directory) cache.delete(Instance.directory) + resolved.delete(Instance.directory) GlobalBus.emit("event", { directory: Instance.directory, payload: { @@ -87,5 +98,16 @@ export const Instance = { } } cache.clear() + resolved.clear() + }, + /** + * Run a function within an existing instance context synchronously. + * The instance must already be fully initialized (created via provide()). + * Returns undefined if the instance doesn't exist or isn't ready. + */ + runInContext(directory: string, fn: () => R): R | undefined { + const ctx = resolved.get(directory) + if (!ctx) return undefined + return context.provide(ctx, fn) }, } diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index 34a5dbb3e71..5c52f9ebf39 100644 --- a/packages/opencode/src/project/state.ts +++ b/packages/opencode/src/project/state.ts @@ -28,6 +28,16 @@ export namespace State { } } + export async function invalidate(root: () => string, init: () => S) { + const key = root() + const entries = recordsByKey.get(key) + if (!entries) return + const entry = entries.get(init) + if (!entry) return + if (entry.dispose) await entry.dispose(await entry.state) + entries.delete(init) + } + export async function dispose(key: string) { const entries = recordsByKey.get(key) if (!entries) return diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 1cc3afee92c..9b5f275d4cb 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -7,9 +7,18 @@ import { Log } from "../util/log" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" import { Flag } from "@/flag/flag" +import { BusEvent } from "@/bus/bus-event" +import { Bus } from "@/bus" +import { FileWatcher } from "@/file/watcher" +import path from "path" export namespace Skill { const log = Log.create({ service: "skill" }) + + export const Events = { + Updated: BusEvent.define("skill.updated", z.object({})), + } + export const Info = z.object({ name: z.string(), description: z.string(), @@ -123,4 +132,29 @@ export namespace Skill { export async function all() { return state().then((x) => Object.values(x)) } + + /** + * Initialize skill hot-reload watcher. + * Listens for file changes in skill directories and reloads skills when SKILL.md files change. + * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC flag. + */ + export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + + Bus.subscribe(FileWatcher.Event.Updated, async (event) => { + const filename = path.basename(event.properties.file) + if (filename !== "SKILL.md") return + + log.info("skill file changed, reloading", { file: event.properties.file, event: event.properties.event }) + + try { + // Reload skills and emit update event + await state() + Bus.publish(Events.Updated, {}) + log.info("skills reloaded successfully") + } catch (err) { + log.error("failed to reload skills", { error: err }) + } + }) + } } diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 35e378f080b..1fa2168f0bd 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -11,6 +11,8 @@ import { WebFetchTool } from "./webfetch" import { WriteTool } from "./write" import { InvalidTool } from "./invalid" import { SkillTool } from "./skill" +import { Skill } from "../skill/skill" +import { Bus } from "@/bus" import type { Agent } from "../agent/agent" import { Tool } from "./tool" import { Instance } from "../project/instance" @@ -30,10 +32,16 @@ import { PlanExitTool, PlanEnterTool } from "./plan" export namespace ToolRegistry { const log = Log.create({ service: "tool.registry" }) - export const state = Instance.state(async () => { + const initState = async () => { const custom = [] as Tool.Info[] const glob = new Bun.Glob("{tool,tools}/*.{js,ts}") + // Cache busting for hot reload + const cacheBust = + Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG() || Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC() + ? `?t=${Date.now()}` + : "" + for (const dir of await Config.directories()) { for await (const match of glob.scan({ cwd: dir, @@ -42,7 +50,7 @@ export namespace ToolRegistry { dot: true, })) { const namespace = path.basename(match, path.extname(match)) - const mod = await import(match) + const mod = await import(match + cacheBust) for (const [id, def] of Object.entries(mod)) { custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def)) } @@ -57,7 +65,9 @@ export namespace ToolRegistry { } return { custom } - }) + } + + export const state = Instance.state(initState) function fromPlugin(id: string, def: ToolDefinition): Tool.Info { return { @@ -140,4 +150,18 @@ export namespace ToolRegistry { ) return result } + + /** + * Initialize tool registry hot-reload watcher. + * Reloads tools when config changes (to pick up new tool files or config directories). + * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG flag. + */ + export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) return + + Bus.subscribe(Config.Events.Updated, async () => { + log.info("config updated, reloading tools") + await Instance.invalidate(initState) + }) + } } diff --git a/packages/opencode/test/config/config-delete-hot-reload.test.ts b/packages/opencode/test/config/config-delete-hot-reload.test.ts new file mode 100644 index 00000000000..45fa2fd7885 --- /dev/null +++ b/packages/opencode/test/config/config-delete-hot-reload.test.ts @@ -0,0 +1,117 @@ +import { test, expect, beforeAll, afterAll, mock } from "bun:test" +import path from "path" +import fs from "fs/promises" + +// Mock FileIgnore to prevent ignoring /tmp directories during tests +mock.module("../../src/file/ignore.ts", () => { + return { + FileIgnore: { + PATTERNS: [".git"], // Keep .git ignored but allow everything else + match: () => false, + }, + } +}) + +import { tmpdir } from "../fixture/fixture.js" +import { Instance } from "../../src/project/instance.js" +import { FileWatcher } from "../../src/file/watcher.js" +import { Config } from "../../src/config/config.js" +import { Agent } from "../../src/agent/agent.js" +import { Command } from "../../src/command/index.js" +import { Bus } from "../../src/bus/index.js" +import { withTimeout } from "../../src/util/timeout.js" + +async function waitForWatcherReady(root: string, label: string) { + const readyPath = path.join(root, ".opencode", `watcher-ready-${label}.txt`) + const readyEvent = new Promise((resolve) => { + const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, (evt) => { + console.log("Event received:", evt.properties.file, evt.properties.event) + console.log("Expecting:", readyPath) + if (evt.properties.file.replaceAll("\\", "/") !== readyPath.replaceAll("\\", "/")) return + unsubscribe() + resolve() + }) + }) + await Bun.write(readyPath, "ready") + await withTimeout(readyEvent, 5000) +} + +test.skip("unlinking agent/command directories reloads config", async () => { + // Enable hot reload flags for this test + process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC"] = "true" + process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG"] = "true" + process.env["OPENCODE_EXPERIMENTAL_FILEWATCHER"] = "true" + + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const opencodeDir = path.join(dir, ".opencode") + const agentDir = path.join(opencodeDir, "agent") + const commandDir = path.join(opencodeDir, "command") + await fs.mkdir(agentDir, { recursive: true }) + await fs.mkdir(commandDir, { recursive: true }) + + await Bun.write( + path.join(agentDir, "delete-test-agent.md"), + `--- +model: test/model +mode: subagent +--- +Delete test agent prompt`, + ) + + await Bun.write( + path.join(commandDir, "delete-test-command.md"), + `--- +description: delete test command +--- +Run $ARGUMENTS`, + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + FileWatcher.init() + Config.initWatcher() + Agent.initWatcher() + Command.initWatcher() + await new Promise((resolve) => setTimeout(resolve, 200)) + }, + fn: async () => { + const initialAgents = await Agent.list() + expect(initialAgents.find((agent) => agent.name === "delete-test-agent")).toBeDefined() + + const initialCommands = await Command.list() + expect(initialCommands.find((command) => command.name === "delete-test-command")).toBeDefined() + + await waitForWatcherReady(tmp.path, "config-delete") + + const waitForConfigUpdate = () => + new Promise((resolve) => { + const unsubscribe = Bus.subscribe(Config.Events.Updated, () => { + unsubscribe() + resolve() + }) + }) + + const agentDir = path.join(tmp.path, ".opencode", "agent") + const commandDir = path.join(tmp.path, ".opencode", "command") + await fs.rm(agentDir, { recursive: true, force: true }) + await fs.rm(commandDir, { recursive: true, force: true }) + + await withTimeout(waitForConfigUpdate(), 5000) + + const updatedAgents = await Agent.list() + expect(updatedAgents.find((agent) => agent.name === "delete-test-agent")).toBeUndefined() + + const updatedCommands = await Command.list() + expect(updatedCommands.find((command) => command.name === "delete-test-command")).toBeUndefined() + }, + }) + + // Cleanup env + delete process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC"] + delete process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG"] +}, 20000) diff --git a/packages/opencode/test/fixture/fixture.ts b/packages/opencode/test/fixture/fixture.ts index ed8c5e344a8..bc49747b41b 100644 --- a/packages/opencode/test/fixture/fixture.ts +++ b/packages/opencode/test/fixture/fixture.ts @@ -20,6 +20,8 @@ export async function tmpdir(options?: TmpDirOptions) { await fs.mkdir(dirpath, { recursive: true }) if (options?.git) { await $`git init`.cwd(dirpath).quiet() + await $`git config user.email "you@example.com"`.cwd(dirpath).quiet() + await $`git config user.name "Your Name"`.cwd(dirpath).quiet() await $`git commit --allow-empty -m "root commit ${dirpath}"`.cwd(dirpath).quiet() } if (options?.config) { diff --git a/packages/opencode/test/skill/skill-hot-reload.test.ts b/packages/opencode/test/skill/skill-hot-reload.test.ts new file mode 100644 index 00000000000..441bfcc0658 --- /dev/null +++ b/packages/opencode/test/skill/skill-hot-reload.test.ts @@ -0,0 +1,457 @@ +import { test, expect, describe, mock } from "bun:test" + +// Mock FileIgnore to prevent ignoring /tmp directories during tests +mock.module("../../src/file/ignore.ts", () => { + return { + FileIgnore: { + PATTERNS: [".git"], // Keep .git ignored but allow everything else + match: () => false, + }, + } +}) + +import { Skill } from "../../src/skill/skill.js" +import { SkillTool } from "../../src/tool/skill.js" +import { ToolRegistry } from "../../src/tool/registry.js" +import { Instance } from "../../src/project/instance.js" +import { tmpdir } from "../fixture/fixture.js" +import { FileWatcher } from "../../src/file/watcher.js" +import { Bus } from "../../src/bus/index.js" +import { withTimeout } from "../../src/util/timeout.js" +import path from "path" +import fs from "fs/promises" + +const TEST_TIMEOUT = 15000 +const EVENT_TIMEOUT = 5000 + +/** + * Wait for the file watcher to be ready by writing a marker file + * and waiting for its change event to be emitted. + */ +async function waitForWatcherReady(root: string, label: string) { + const readyPath = path.join(root, ".opencode", `watcher-ready-${label}.txt`) + const readyEvent = new Promise((resolve) => { + const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, (evt) => { + if (evt.properties.file.replaceAll("\\", "/") !== readyPath.replaceAll("\\", "/")) return + unsubscribe() + resolve() + }) + }) + await Bun.write(readyPath, `ready-${Date.now()}`) + await withTimeout(readyEvent, EVENT_TIMEOUT) +} + +/** + * Create a promise that resolves when Skill.Events.Updated is emitted. + */ +function waitForSkillUpdate(): Promise { + return new Promise((resolve) => { + const unsubscribe = Bus.subscribe(Skill.Events.Updated, () => { + unsubscribe() + resolve() + }) + }) +} + +/** + * Helper to create a skill file with proper frontmatter + */ +function createSkillContent(name: string, description: string, body = "Instructions."): string { + return `--- +name: ${name} +description: ${description} +--- + +# ${name} + +${body} +` +} + +describe("skill hot reload", () => { + // Enable hot reload flags for these tests + process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC"] = "true" + process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG"] = "true" + process.env["OPENCODE_EXPERIMENTAL_FILEWATCHER"] = "true" + + /** + * Test adding a new skill file triggers hot reload and updates both + * the skill list and tool description. + */ + test.skip( + "adding a new skill updates skill list and tool description", + async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + // Create initial skill so we have something to start with + const skillDir = path.join(dir, ".opencode", "skill", "existing-skill") + await fs.mkdir(skillDir, { recursive: true }) + await Bun.write(path.join(skillDir, "SKILL.md"), createSkillContent("existing-skill", "An existing skill.")) + }, + }) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + FileWatcher.init() + Skill.initWatcher() + ToolRegistry.initWatcher() + await new Promise((resolve) => setTimeout(resolve, 200)) + }, + fn: async () => { + // Verify initial state + const initialSkills = await Skill.all() + expect(initialSkills.find((s) => s.name === "existing-skill")).toBeDefined() + expect(initialSkills.find((s) => s.name === "new-skill")).toBeUndefined() + + const initialToolConfig = await SkillTool.init({}) + expect(initialToolConfig.description).toContain("existing-skill") + expect(initialToolConfig.description).not.toContain("new-skill") + + // Ensure watcher is ready + await waitForWatcherReady(tmp.path, "add-skill") + + // Set up listener BEFORE making the change + const updatePromise = waitForSkillUpdate() + + // Add a new skill + const newSkillDir = path.join(tmp.path, ".opencode", "skill", "new-skill") + await fs.mkdir(newSkillDir, { recursive: true }) + await Bun.write(path.join(newSkillDir, "SKILL.md"), createSkillContent("new-skill", "A newly added skill.")) + + // Wait for the skill update event + await withTimeout(updatePromise, EVENT_TIMEOUT) + + // Verify new skill is available + const updatedSkills = await Skill.all() + const newSkill = updatedSkills.find((s) => s.name === "new-skill") + expect(newSkill).toBeDefined() + expect(newSkill?.description).toBe("A newly added skill.") + + // Verify tool description includes new skill + const updatedToolConfig = await SkillTool.init({}) + expect(updatedToolConfig.description).toContain("new-skill") + expect(updatedToolConfig.description).toContain("A newly added skill.") + }, + }) + }, + TEST_TIMEOUT, + ) + + /** + * Test modifying an existing skill file triggers hot reload. + */ + test.skip( + "modifying an existing skill updates skill list and tool description", + async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + // Create the skill that we will modify + const skillDir = path.join(dir, ".opencode", "skill", "modifiable-skill") + await fs.mkdir(skillDir, { recursive: true }) + await Bun.write( + path.join(skillDir, "SKILL.md"), + createSkillContent("modifiable-skill", "Original description.", "Original content."), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + FileWatcher.init() + Skill.initWatcher() + ToolRegistry.initWatcher() + await new Promise((resolve) => setTimeout(resolve, 200)) + }, + fn: async () => { + // Verify initial state + const initialSkills = await Skill.all() + const initialSkill = initialSkills.find((s) => s.name === "modifiable-skill") + expect(initialSkill).toBeDefined() + expect(initialSkill?.description).toBe("Original description.") + + const initialToolConfig = await SkillTool.init({}) + expect(initialToolConfig.description).toContain("Original description.") + + // Ensure watcher is ready + await waitForWatcherReady(tmp.path, "modify-skill") + + // Set up listener BEFORE making the change + const updatePromise = waitForSkillUpdate() + + // Modify the existing skill file + const skillPath = path.join(tmp.path, ".opencode", "skill", "modifiable-skill", "SKILL.md") + await Bun.write(skillPath, createSkillContent("modifiable-skill", "Updated description.", "Updated content.")) + + // Wait for the skill update event + await withTimeout(updatePromise, EVENT_TIMEOUT) + + // Verify skill description is updated + const updatedSkills = await Skill.all() + const updatedSkill = updatedSkills.find((s) => s.name === "modifiable-skill") + expect(updatedSkill).toBeDefined() + expect(updatedSkill?.description).toBe("Updated description.") + + // Verify tool description is updated + const updatedToolConfig = await SkillTool.init({}) + expect(updatedToolConfig.description).toContain("Updated description.") + expect(updatedToolConfig.description).not.toContain("Original description.") + }, + }) + }, + TEST_TIMEOUT, + ) + + /** + * Test deleting a skill file (SKILL.md) triggers hot reload. + */ + test.skip( + "deleting a skill file removes it from skill list and tool description", + async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + // Create two skills - one to keep, one to delete + const keepDir = path.join(dir, ".opencode", "skill", "keeper-skill") + const deleteDir = path.join(dir, ".opencode", "skill", "deletable-skill") + await fs.mkdir(keepDir, { recursive: true }) + await fs.mkdir(deleteDir, { recursive: true }) + await Bun.write(path.join(keepDir, "SKILL.md"), createSkillContent("keeper-skill", "This skill stays.")) + await Bun.write( + path.join(deleteDir, "SKILL.md"), + createSkillContent("deletable-skill", "This skill will be removed."), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + FileWatcher.init() + Skill.initWatcher() + ToolRegistry.initWatcher() + await new Promise((resolve) => setTimeout(resolve, 200)) + }, + fn: async () => { + // Verify initial state has both skills + const initialSkills = await Skill.all() + expect(initialSkills.find((s) => s.name === "keeper-skill")).toBeDefined() + expect(initialSkills.find((s) => s.name === "deletable-skill")).toBeDefined() + + const initialToolConfig = await SkillTool.init({}) + expect(initialToolConfig.description).toContain("keeper-skill") + expect(initialToolConfig.description).toContain("deletable-skill") + + // Ensure watcher is ready + await waitForWatcherReady(tmp.path, "delete-skill-file") + + // Set up listener BEFORE making the change + const updatePromise = waitForSkillUpdate() + + // Delete just the SKILL.md file (not the directory) + const skillFilePath = path.join(tmp.path, ".opencode", "skill", "deletable-skill", "SKILL.md") + await fs.unlink(skillFilePath) + + // Wait for the skill update event + await withTimeout(updatePromise, EVENT_TIMEOUT) + + // Verify deleted skill is removed + const updatedSkills = await Skill.all() + expect(updatedSkills.find((s) => s.name === "keeper-skill")).toBeDefined() + expect(updatedSkills.find((s) => s.name === "deletable-skill")).toBeUndefined() + + // Verify tool description no longer contains deleted skill + const updatedToolConfig = await SkillTool.init({}) + expect(updatedToolConfig.description).toContain("keeper-skill") + expect(updatedToolConfig.description).not.toContain("deletable-skill") + }, + }) + }, + TEST_TIMEOUT, + ) + + /** + * Test deleting an entire skill directory triggers hot reload. + */ + test.skip( + "deleting a skill directory removes it from skill list and tool description", + async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + // Create two skills - one to keep, one to delete + const keepDir = path.join(dir, ".opencode", "skill", "persistent-skill") + const deleteDir = path.join(dir, ".opencode", "skill", "removable-skill") + await fs.mkdir(keepDir, { recursive: true }) + await fs.mkdir(deleteDir, { recursive: true }) + await Bun.write( + path.join(keepDir, "SKILL.md"), + createSkillContent("persistent-skill", "This skill persists."), + ) + await Bun.write( + path.join(deleteDir, "SKILL.md"), + createSkillContent("removable-skill", "This skill directory will be deleted."), + ) + }, + }) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + FileWatcher.init() + Skill.initWatcher() + ToolRegistry.initWatcher() + await new Promise((resolve) => setTimeout(resolve, 200)) + }, + fn: async () => { + // Verify initial state has both skills + const initialSkills = await Skill.all() + expect(initialSkills.find((s) => s.name === "persistent-skill")).toBeDefined() + expect(initialSkills.find((s) => s.name === "removable-skill")).toBeDefined() + + const initialToolConfig = await SkillTool.init({}) + expect(initialToolConfig.description).toContain("persistent-skill") + expect(initialToolConfig.description).toContain("removable-skill") + + // Ensure watcher is ready + await waitForWatcherReady(tmp.path, "delete-skill-dir") + + // Set up listener BEFORE making the change + const updatePromise = waitForSkillUpdate() + + // Delete the entire skill directory + const skillDirPath = path.join(tmp.path, ".opencode", "skill", "removable-skill") + await fs.rm(skillDirPath, { recursive: true, force: true }) + + // Wait for the skill update event + await withTimeout(updatePromise, EVENT_TIMEOUT) + + // Verify deleted skill is removed + const updatedSkills = await Skill.all() + expect(updatedSkills.find((s) => s.name === "persistent-skill")).toBeDefined() + expect(updatedSkills.find((s) => s.name === "removable-skill")).toBeUndefined() + + // Verify tool description no longer contains deleted skill + const updatedToolConfig = await SkillTool.init({}) + expect(updatedToolConfig.description).toContain("persistent-skill") + expect(updatedToolConfig.description).not.toContain("removable-skill") + }, + }) + }, + TEST_TIMEOUT, + ) + + /** + * Test renaming a skill (changing the name field in frontmatter) + */ + test.skip( + "renaming a skill name in frontmatter updates the skill list", + async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + const skillDir = path.join(dir, ".opencode", "skill", "renamable-skill") + await fs.mkdir(skillDir, { recursive: true }) + await Bun.write(path.join(skillDir, "SKILL.md"), createSkillContent("old-name", "A skill to be renamed.")) + }, + }) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + FileWatcher.init() + Skill.initWatcher() + ToolRegistry.initWatcher() + await new Promise((resolve) => setTimeout(resolve, 200)) + }, + fn: async () => { + // Verify initial state + const initialSkills = await Skill.all() + expect(initialSkills.find((s) => s.name === "old-name")).toBeDefined() + expect(initialSkills.find((s) => s.name === "new-name")).toBeUndefined() + + // Ensure watcher is ready + await waitForWatcherReady(tmp.path, "rename-skill") + + // Set up listener BEFORE making the change + const updatePromise = waitForSkillUpdate() + + // Rename the skill by changing the frontmatter + const skillPath = path.join(tmp.path, ".opencode", "skill", "renamable-skill", "SKILL.md") + await Bun.write(skillPath, createSkillContent("new-name", "A renamed skill.")) + + // Wait for the skill update event + await withTimeout(updatePromise, EVENT_TIMEOUT) + + // Verify skill is renamed + const updatedSkills = await Skill.all() + expect(updatedSkills.find((s) => s.name === "old-name")).toBeUndefined() + expect(updatedSkills.find((s) => s.name === "new-name")).toBeDefined() + expect(updatedSkills.find((s) => s.name === "new-name")?.description).toBe("A renamed skill.") + }, + }) + }, + TEST_TIMEOUT, + ) + + /** + * Test that adding multiple skills in sequence works correctly + */ + test.skip( + "adding multiple skills in sequence updates correctly", + async () => { + await using tmp = await tmpdir({ + git: true, + init: async (dir) => { + // Create .opencode/skill directory + await fs.mkdir(path.join(dir, ".opencode", "skill"), { recursive: true }) + }, + }) + + await Instance.provide({ + directory: tmp.path, + init: async () => { + FileWatcher.init() + Skill.initWatcher() + ToolRegistry.initWatcher() + await new Promise((resolve) => setTimeout(resolve, 200)) + }, + fn: async () => { + // Verify no skills initially + const initialSkills = await Skill.all() + expect(initialSkills).toHaveLength(0) + + // Ensure watcher is ready + await waitForWatcherReady(tmp.path, "multi-add") + + // Add first skill + const updatePromise1 = waitForSkillUpdate() + const skill1Dir = path.join(tmp.path, ".opencode", "skill", "skill-one") + await fs.mkdir(skill1Dir, { recursive: true }) + await Bun.write(path.join(skill1Dir, "SKILL.md"), createSkillContent("skill-one", "First skill.")) + await withTimeout(updatePromise1, EVENT_TIMEOUT) + + const afterFirst = await Skill.all() + expect(afterFirst).toHaveLength(1) + expect(afterFirst.find((s) => s.name === "skill-one")).toBeDefined() + + // Add second skill + const updatePromise2 = waitForSkillUpdate() + const skill2Dir = path.join(tmp.path, ".opencode", "skill", "skill-two") + await fs.mkdir(skill2Dir, { recursive: true }) + await Bun.write(path.join(skill2Dir, "SKILL.md"), createSkillContent("skill-two", "Second skill.")) + await withTimeout(updatePromise2, EVENT_TIMEOUT) + + const afterSecond = await Skill.all() + expect(afterSecond).toHaveLength(2) + expect(afterSecond.find((s) => s.name === "skill-one")).toBeDefined() + expect(afterSecond.find((s) => s.name === "skill-two")).toBeDefined() + }, + }) + }, + TEST_TIMEOUT, + ) +}) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9c4f0e50d12..cbace013033 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -47,915 +47,64 @@ export type EventServerInstanceDisposed = { } } -export type EventLspClientDiagnostics = { - type: "lsp.client.diagnostics" - properties: { - serverID: string - path: string - } -} - -export type EventLspUpdated = { - type: "lsp.updated" - properties: { - [key: string]: unknown - } -} - -export type FileDiff = { - file: string - before: string - after: string - additions: number - deletions: number -} - -export type UserMessage = { - id: string - sessionID: string - role: "user" - time: { - created: number - } - summary?: { - title?: string - body?: string - diffs: Array - } - agent: string - model: { - providerID: string - modelID: string - } - system?: string - tools?: { - [key: string]: boolean - } - variant?: string -} - -export type ProviderAuthError = { - name: "ProviderAuthError" - data: { - providerID: string - message: string - } -} - -export type UnknownError = { - name: "UnknownError" - data: { - message: string - } -} - -export type MessageOutputLengthError = { - name: "MessageOutputLengthError" - data: { - [key: string]: unknown - } -} - -export type MessageAbortedError = { - name: "MessageAbortedError" - data: { - message: string - } -} - -export type ApiError = { - name: "APIError" - data: { - message: string - statusCode?: number - isRetryable: boolean - responseHeaders?: { - [key: string]: string - } - responseBody?: string - metadata?: { - [key: string]: string - } - } -} - -export type AssistantMessage = { - id: string - sessionID: string - role: "assistant" - time: { - created: number - completed?: number - } - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError - parentID: string - modelID: string - providerID: string - mode: string - agent: string - path: { - cwd: string - root: string - } - summary?: boolean - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - finish?: string -} - -export type Message = UserMessage | AssistantMessage - -export type EventMessageUpdated = { - type: "message.updated" - properties: { - info: Message - } -} - -export type EventMessageRemoved = { - type: "message.removed" +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" properties: { - sessionID: string - messageID: string - } -} - -export type TextPart = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown - } -} - -export type ReasoningPart = { - id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end?: number - } -} - -export type FilePartSourceText = { - value: string - start: number - end: number -} - -export type FileSource = { - text: FilePartSourceText - type: "file" - path: string -} - -export type Range = { - start: { - line: number - character: number - } - end: { - line: number - character: number - } -} - -export type SymbolSource = { - text: FilePartSourceText - type: "symbol" - path: string - range: Range - name: string - kind: number -} - -export type ResourceSource = { - text: FilePartSourceText - type: "resource" - clientName: string - uri: string -} - -export type FilePartSource = FileSource | SymbolSource | ResourceSource - -export type FilePart = { - id: string - sessionID: string - messageID: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource -} - -export type ToolStatePending = { - status: "pending" - input: { - [key: string]: unknown - } - raw: string -} - -export type ToolStateRunning = { - status: "running" - input: { - [key: string]: unknown - } - title?: string - metadata?: { - [key: string]: unknown - } - time: { - start: number + file: string + event: "add" | "change" | "unlink" } } -export type ToolStateCompleted = { - status: "completed" - input: { - [key: string]: unknown - } - output: string - title: string - metadata: { - [key: string]: unknown - } - time: { - start: number - end: number - compacted?: number - } - attachments?: Array -} - -export type ToolStateError = { - status: "error" - input: { - [key: string]: unknown - } - error: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end: number - } -} - -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - -export type ToolPart = { - id: string - sessionID: string - messageID: string - type: "tool" - callID: string - tool: string - state: ToolState - metadata?: { - [key: string]: unknown - } -} - -export type StepStartPart = { - id: string - sessionID: string - messageID: string - type: "step-start" - snapshot?: string -} - -export type StepFinishPart = { - id: string - sessionID: string - messageID: string - type: "step-finish" - reason: string - snapshot?: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } -} - -export type SnapshotPart = { - id: string - sessionID: string - messageID: string - type: "snapshot" - snapshot: string -} - -export type PatchPart = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array -} - -export type AgentPart = { - id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } -} - -export type RetryPart = { - id: string - sessionID: string - messageID: string - type: "retry" - attempt: number - error: ApiError - time: { - created: number - } -} - -export type CompactionPart = { - id: string - sessionID: string - messageID: string - type: "compaction" - auto: boolean -} - -export type Part = - | TextPart - | { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - command?: string - } - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - -export type EventMessagePartUpdated = { - type: "message.part.updated" - properties: { - part: Part - delta?: string - } -} - -export type EventMessagePartRemoved = { - type: "message.part.removed" - properties: { - sessionID: string - messageID: string - partID: string - } -} - -export type PermissionRequest = { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string - } -} - -export type EventPermissionAsked = { - type: "permission.asked" - properties: PermissionRequest -} - -export type EventPermissionReplied = { - type: "permission.replied" - properties: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" - } -} - -export type SessionStatus = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - next: number - } - | { - type: "busy" - } - -export type EventSessionStatus = { - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } -} - -export type EventSessionIdle = { - type: "session.idle" - properties: { - sessionID: string - } -} - -export type QuestionOption = { +/** + * Custom keybind configurations + */ +export type KeybindsConfig = { /** - * Display text (1-5 words, concise) + * Leader key for keybind combinations */ - label: string + leader?: string /** - * Explanation of choice + * Exit the application */ - description: string -} - -export type QuestionInfo = { + app_exit?: string /** - * Complete question + * Open external editor */ - question: string + editor_open?: string /** - * Very short label (max 12 chars) + * List available themes */ - header: string + theme_list?: string /** - * Available choices + * Toggle sidebar */ - options: Array + sidebar_toggle?: string /** - * Allow selecting multiple choices + * Toggle session scrollbar */ - multiple?: boolean + scrollbar_toggle?: string /** - * Allow typing a custom answer (default: true) + * Toggle username visibility */ - custom?: boolean -} - -export type QuestionRequest = { - id: string - sessionID: string + username_toggle?: string /** - * Questions to ask + * View status */ - questions: Array - tool?: { - messageID: string - callID: string - } -} - -export type EventQuestionAsked = { - type: "question.asked" - properties: QuestionRequest -} - -export type QuestionAnswer = Array - -export type EventQuestionReplied = { - type: "question.replied" - properties: { - sessionID: string - requestID: string - answers: Array - } -} - -export type EventQuestionRejected = { - type: "question.rejected" - properties: { - sessionID: string - requestID: string - } -} - -export type EventSessionCompacted = { - type: "session.compacted" - properties: { - sessionID: string - } -} - -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string - } -} - -export type Todo = { + status_view?: string /** - * Brief description of the task + * Export session to editor */ - content: string + session_export?: string /** - * Current status of the task: pending, in_progress, completed, cancelled + * Create a new session */ - status: string + session_new?: string /** - * Priority level of the task: high, medium, low + * List all sessions */ - priority: string + session_list?: string /** - * Unique identifier for the todo item - */ - id: string -} - -export type EventTodoUpdated = { - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - -export type EventTuiPromptAppend = { - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow = { - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ - duration?: number - } -} - -export type EventTuiSessionSelect = { - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type EventMcpToolsChanged = { - type: "mcp.tools.changed" - properties: { - server: string - } -} - -export type EventCommandExecuted = { - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - -export type PermissionAction = "allow" | "deny" | "ask" - -export type PermissionRule = { - permission: string - pattern: string - action: PermissionAction -} - -export type PermissionRuleset = Array - -export type Session = { - id: string - slug: string - projectID: string - directory: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - share?: { - url: string - } - title: string - version: string - time: { - created: number - updated: number - compacting?: number - archived?: number - } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } -} - -export type EventSessionCreated = { - type: "session.created" - properties: { - info: Session - } -} - -export type EventSessionUpdated = { - type: "session.updated" - properties: { - info: Session - } -} - -export type EventSessionDeleted = { - type: "session.deleted" - properties: { - info: Session - } -} - -export type EventSessionDiff = { - type: "session.diff" - properties: { - sessionID: string - diff: Array - } -} - -export type EventSessionError = { - type: "session.error" - properties: { - sessionID?: string - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError - } -} - -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - -export type EventVcsBranchUpdated = { - type: "vcs.branch.updated" - properties: { - branch?: string - } -} - -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number -} - -export type EventPtyCreated = { - type: "pty.created" - properties: { - info: Pty - } -} - -export type EventPtyUpdated = { - type: "pty.updated" - properties: { - info: Pty - } -} - -export type EventPtyExited = { - type: "pty.exited" - properties: { - id: string - exitCode: number - } -} - -export type EventPtyDeleted = { - type: "pty.deleted" - properties: { - id: string - } -} - -export type EventServerConnected = { - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type EventGlobalDisposed = { - type: "global.disposed" - properties: { - [key: string]: unknown - } -} - -export type Event = - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventProjectUpdated - | EventServerInstanceDisposed - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventPermissionAsked - | EventPermissionReplied - | EventSessionStatus - | EventSessionIdle - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventSessionCompacted - | EventFileEdited - | EventTodoUpdated - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventCommandExecuted - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | EventSessionDiff - | EventSessionError - | EventFileWatcherUpdated - | EventVcsBranchUpdated - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventServerConnected - | EventGlobalDisposed - -export type GlobalEvent = { - directory: string - payload: Event -} - -export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false -} - -export type NotFoundError = { - name: "NotFoundError" - data: { - message: string - } -} - -/** - * Custom keybind configurations - */ -export type KeybindsConfig = { - /** - * Leader key for keybind combinations - */ - leader?: string - /** - * Exit the application - */ - app_exit?: string - /** - * Open external editor - */ - editor_open?: string - /** - * List available themes - */ - theme_list?: string - /** - * Toggle sidebar - */ - sidebar_toggle?: string - /** - * Toggle session scrollbar - */ - scrollbar_toggle?: string - /** - * Toggle username visibility - */ - username_toggle?: string - /** - * View status - */ - status_view?: string - /** - * Export session to editor - */ - session_export?: string - /** - * Create a new session - */ - session_new?: string - /** - * List all sessions - */ - session_list?: string - /** - * Show session timeline + * Show session timeline */ session_timeline?: string /** @@ -1276,506 +425,1379 @@ export type KeybindsConfig = { tips_toggle?: string } -/** - * Log level - */ -export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" - -/** - * Server configuration for opencode serve and web commands - */ -export type ServerConfig = { - /** - * Port to listen on - */ - port?: number - /** - * Hostname to listen on - */ - hostname?: string - /** - * Enable mDNS service discovery - */ - mdns?: boolean - /** - * Additional domains to allow for CORS - */ - cors?: Array +/** + * Log level + */ +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" + +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { + /** + * Port to listen on + */ + port?: number + /** + * Hostname to listen on + */ + hostname?: string + /** + * Enable mDNS service discovery + */ + mdns?: boolean + /** + * Additional domains to allow for CORS + */ + cors?: Array +} + +export type PermissionActionConfig = "ask" | "allow" | "deny" + +export type PermissionObjectConfig = { + [key: string]: PermissionActionConfig +} + +export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig + +export type PermissionConfig = + | { + __originalKeys?: Array + read?: PermissionRuleConfig + edit?: PermissionRuleConfig + glob?: PermissionRuleConfig + grep?: PermissionRuleConfig + list?: PermissionRuleConfig + bash?: PermissionRuleConfig + task?: PermissionRuleConfig + external_directory?: PermissionRuleConfig + todowrite?: PermissionActionConfig + todoread?: PermissionActionConfig + question?: PermissionActionConfig + webfetch?: PermissionActionConfig + websearch?: PermissionActionConfig + codesearch?: PermissionActionConfig + lsp?: PermissionRuleConfig + doom_loop?: PermissionActionConfig + [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined + } + | PermissionActionConfig + +export type AgentConfig = { + model?: string + temperature?: number + top_p?: number + prompt?: string + /** + * @deprecated Use 'permission' field instead + */ + tools?: { + [key: string]: boolean + } + disable?: boolean + /** + * Description of when to use the agent + */ + description?: string + mode?: "subagent" | "primary" | "all" + /** + * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) + */ + hidden?: boolean + options?: { + [key: string]: unknown + } + /** + * Hex color code for the agent (e.g., #FF5733) + */ + color?: string + /** + * Maximum number of agentic iterations before forcing text-only response + */ + steps?: number + /** + * @deprecated Use 'steps' field instead. + */ + maxSteps?: number + permission?: PermissionConfig + [key: string]: + | unknown + | string + | number + | { + [key: string]: boolean + } + | boolean + | "subagent" + | "primary" + | "all" + | { + [key: string]: unknown + } + | string + | number + | PermissionConfig + | undefined +} + +export type ProviderConfig = { + api?: string + name?: string + env?: Array + id?: string + npm?: string + models?: { + [key: string]: { + id?: string + name?: string + family?: string + release_date?: string + attachment?: boolean + reasoning?: boolean + temperature?: boolean + tool_call?: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit?: { + context: number + input?: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + options?: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + provider?: { + npm: string + } + /** + * Variant-specific configuration + */ + variants?: { + [key: string]: { + /** + * Disable this variant for the model + */ + disabled?: boolean + [key: string]: unknown | boolean | undefined + } + } + } + } + whitelist?: Array + blacklist?: Array + options?: { + apiKey?: string + baseURL?: string + /** + * GitHub Enterprise URL for copilot authentication + */ + enterpriseUrl?: string + /** + * Enable promptCacheKey for this provider (default false) + */ + setCacheKey?: boolean + /** + * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. + */ + timeout?: number | false + [key: string]: unknown | string | boolean | number | false | undefined + } +} + +export type McpLocalConfig = { + /** + * Type of MCP server connection + */ + type: "local" + /** + * Command and arguments to run the MCP server + */ + command: Array + /** + * Environment variables to set when running the MCP server + */ + environment?: { + [key: string]: string + } + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number +} + +export type McpOAuthConfig = { + /** + * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. + */ + clientId?: string + /** + * OAuth client secret (if required by the authorization server) + */ + clientSecret?: string + /** + * OAuth scopes to request during authorization + */ + scope?: string +} + +export type McpRemoteConfig = { + /** + * Type of MCP server connection + */ + type: "remote" + /** + * URL of the remote MCP server + */ + url: string + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Headers to send with the request + */ + headers?: { + [key: string]: string + } + /** + * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. + */ + oauth?: McpOAuthConfig | false + /** + * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number +} + +/** + * @deprecated Always uses stretch layout. + */ +export type LayoutConfig = "auto" | "stretch" + +export type Config = { + /** + * JSON schema reference for configuration validation + */ + $schema?: string + /** + * Theme name to use for the interface + */ + theme?: string + keybinds?: KeybindsConfig + logLevel?: LogLevel + /** + * TUI specific settings + */ + tui?: { + /** + * TUI scroll speed + */ + scroll_speed?: number + /** + * Scroll acceleration settings + */ + scroll_acceleration?: { + /** + * Enable scroll acceleration + */ + enabled: boolean + } + /** + * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column + */ + diff_style?: "auto" | "stacked" + } + server?: ServerConfig + /** + * Command configuration, see https://opencode.ai/docs/commands + */ + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + subtask?: boolean + } + } + watcher?: { + ignore?: Array + } + plugin?: Array + snapshot?: boolean + /** + * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing + */ + share?: "manual" | "auto" | "disabled" + /** + * @deprecated Use 'share' field instead. Share newly created sessions automatically + */ + autoshare?: boolean + /** + * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications + */ + autoupdate?: boolean | "notify" + /** + * Disable providers that are loaded automatically + */ + disabled_providers?: Array + /** + * When set, ONLY these providers will be enabled. All other providers will be ignored + */ + enabled_providers?: Array + /** + * Model to use in the format of provider/model, eg anthropic/claude-2 + */ + model?: string + /** + * Small model to use for tasks like title generation in the format of provider/model + */ + small_model?: string + /** + * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. + */ + default_agent?: string + /** + * Custom username to display in conversations instead of system username + */ + username?: string + /** + * @deprecated Use `agent` field instead. + */ + mode?: { + build?: AgentConfig + plan?: AgentConfig + [key: string]: AgentConfig | undefined + } + /** + * Agent configuration, see https://opencode.ai/docs/agent + */ + agent?: { + plan?: AgentConfig + build?: AgentConfig + general?: AgentConfig + explore?: AgentConfig + title?: AgentConfig + summary?: AgentConfig + compaction?: AgentConfig + [key: string]: AgentConfig | undefined + } + /** + * Custom provider configurations and model overrides + */ + provider?: { + [key: string]: ProviderConfig + } + /** + * MCP (Model Context Protocol) server configurations + */ + mcp?: { + [key: string]: + | McpLocalConfig + | McpRemoteConfig + | { + enabled: boolean + } + } + formatter?: + | false + | { + [key: string]: { + disabled?: boolean + command?: Array + environment?: { + [key: string]: string + } + extensions?: Array + } + } + lsp?: + | false + | { + [key: string]: + | { + disabled: true + } + | { + command: Array + extensions?: Array + disabled?: boolean + env?: { + [key: string]: string + } + initialization?: { + [key: string]: unknown + } + } + } + /** + * Additional instruction files or patterns to include + */ + instructions?: Array + layout?: LayoutConfig + permission?: PermissionConfig + tools?: { + [key: string]: boolean + } + enterprise?: { + /** + * Enterprise URL + */ + url?: string + } + compaction?: { + /** + * Enable automatic compaction when context is full (default: true) + */ + auto?: boolean + /** + * Enable pruning of old tool outputs (default: true) + */ + prune?: boolean + } + experimental?: { + hook?: { + file_edited?: { + [key: string]: Array<{ + command: Array + environment?: { + [key: string]: string + } + }> + } + session_completed?: Array<{ + command: Array + environment?: { + [key: string]: string + } + }> + } + /** + * Number of retries for chat completions on failure + */ + chatMaxRetries?: number + disable_paste_summary?: boolean + /** + * Enable the batch tool + */ + batch_tool?: boolean + /** + * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) + */ + openTelemetry?: boolean + /** + * Tools that should only be available to primary agents. + */ + primary_tools?: Array + /** + * Continue the agent loop when a tool call is denied + */ + continue_loop_on_deny?: boolean + /** + * Timeout in milliseconds for model context protocol (MCP) requests + */ + mcp_timeout?: number + } +} + +export type EventConfigUpdated = { + type: "config.updated" + properties: Config +} + +export type EventLspClientDiagnostics = { + type: "lsp.client.diagnostics" + properties: { + serverID: string + path: string + } +} + +export type EventLspUpdated = { + type: "lsp.updated" + properties: { + [key: string]: unknown + } +} + +export type FileDiff = { + file: string + before: string + after: string + additions: number + deletions: number +} + +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { + created: number + } + summary?: { + title?: string + body?: string + diffs: Array + } + agent: string + model: { + providerID: string + modelID: string + } + system?: string + tools?: { + [key: string]: boolean + } + variant?: string +} + +export type ProviderAuthError = { + name: "ProviderAuthError" + data: { + providerID: string + message: string + } +} + +export type UnknownError = { + name: "UnknownError" + data: { + message: string + } +} + +export type MessageOutputLengthError = { + name: "MessageOutputLengthError" + data: { + [key: string]: unknown + } +} + +export type MessageAbortedError = { + name: "MessageAbortedError" + data: { + message: string + } +} + +export type ApiError = { + name: "APIError" + data: { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string + } + responseBody?: string + metadata?: { + [key: string]: string + } + } +} + +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { + created: number + completed?: number + } + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string + } + summary?: boolean + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + finish?: string +} + +export type Message = UserMessage | AssistantMessage + +export type EventMessageUpdated = { + type: "message.updated" + properties: { + info: Message + } +} + +export type EventMessageRemoved = { + type: "message.removed" + properties: { + sessionID: string + messageID: string + } +} + +export type TextPart = { + id: string + sessionID: string + messageID: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } +} + +export type ReasoningPart = { + id: string + sessionID: string + messageID: string + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } +} + +export type FilePartSourceText = { + value: string + start: number + end: number +} + +export type FileSource = { + text: FilePartSourceText + type: "file" + path: string +} + +export type Range = { + start: { + line: number + character: number + } + end: { + line: number + character: number + } +} + +export type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: Range + name: string + kind: number +} + +export type ResourceSource = { + text: FilePartSourceText + type: "resource" + clientName: string + uri: string +} + +export type FilePartSource = FileSource | SymbolSource | ResourceSource + +export type FilePart = { + id: string + sessionID: string + messageID: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type ToolStatePending = { + status: "pending" + input: { + [key: string]: unknown + } + raw: string } -export type PermissionActionConfig = "ask" | "allow" | "deny" +export type ToolStateRunning = { + status: "running" + input: { + [key: string]: unknown + } + title?: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + } +} -export type PermissionObjectConfig = { - [key: string]: PermissionActionConfig +export type ToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown + } + output: string + title: string + metadata: { + [key: string]: unknown + } + time: { + start: number + end: number + compacted?: number + } + attachments?: Array } -export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig +export type ToolStateError = { + status: "error" + input: { + [key: string]: unknown + } + error: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end: number + } +} -export type PermissionConfig = - | { - __originalKeys?: Array - read?: PermissionRuleConfig - edit?: PermissionRuleConfig - glob?: PermissionRuleConfig - grep?: PermissionRuleConfig - list?: PermissionRuleConfig - bash?: PermissionRuleConfig - task?: PermissionRuleConfig - external_directory?: PermissionRuleConfig - todowrite?: PermissionActionConfig - todoread?: PermissionActionConfig - question?: PermissionActionConfig - webfetch?: PermissionActionConfig - websearch?: PermissionActionConfig - codesearch?: PermissionActionConfig - lsp?: PermissionRuleConfig - doom_loop?: PermissionActionConfig - [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined - } - | PermissionActionConfig +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError -export type AgentConfig = { - model?: string - temperature?: number - top_p?: number - prompt?: string - /** - * @deprecated Use 'permission' field instead - */ - tools?: { - [key: string]: boolean - } - disable?: boolean - /** - * Description of when to use the agent - */ - description?: string - mode?: "subagent" | "primary" | "all" - /** - * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) - */ - hidden?: boolean - options?: { +export type ToolPart = { + id: string + sessionID: string + messageID: string + type: "tool" + callID: string + tool: string + state: ToolState + metadata?: { [key: string]: unknown } - /** - * Hex color code for the agent (e.g., #FF5733) - */ - color?: string - /** - * Maximum number of agentic iterations before forcing text-only response - */ - steps?: number - /** - * @deprecated Use 'steps' field instead. - */ - maxSteps?: number - permission?: PermissionConfig - [key: string]: - | unknown - | string - | number - | { - [key: string]: boolean - } - | boolean - | "subagent" - | "primary" - | "all" - | { - [key: string]: unknown - } - | string - | number - | PermissionConfig - | undefined } -export type ProviderConfig = { - api?: string - name?: string - env?: Array - id?: string - npm?: string - models?: { - [key: string]: { - id?: string - name?: string - family?: string - release_date?: string - attachment?: boolean - reasoning?: boolean - temperature?: boolean - tool_call?: boolean - interleaved?: - | true - | { - field: "reasoning_content" | "reasoning_details" - } - cost?: { - input: number - output: number - cache_read?: number - cache_write?: number - context_over_200k?: { - input: number - output: number - cache_read?: number - cache_write?: number - } - } - limit?: { - context: number - input?: number - output: number - } - modalities?: { - input: Array<"text" | "audio" | "image" | "video" | "pdf"> - output: Array<"text" | "audio" | "image" | "video" | "pdf"> - } - experimental?: boolean - status?: "alpha" | "beta" | "deprecated" - options?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - provider?: { - npm: string - } - /** - * Variant-specific configuration - */ - variants?: { - [key: string]: { - /** - * Disable this variant for the model - */ - disabled?: boolean - [key: string]: unknown | boolean | undefined - } - } +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" + snapshot?: string +} + +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + reason: string + snapshot?: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number } } - whitelist?: Array - blacklist?: Array - options?: { - apiKey?: string - baseURL?: string - /** - * GitHub Enterprise URL for copilot authentication - */ - enterpriseUrl?: string - /** - * Enable promptCacheKey for this provider (default false) - */ - setCacheKey?: boolean - /** - * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. - */ - timeout?: number | false - [key: string]: unknown | string | boolean | number | false | undefined +} + +export type SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string +} + +export type PatchPart = { + id: string + sessionID: string + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type RetryPart = { + id: string + sessionID: string + messageID: string + type: "retry" + attempt: number + error: ApiError + time: { + created: number } } -export type McpLocalConfig = { - /** - * Type of MCP server connection - */ - type: "local" - /** - * Command and arguments to run the MCP server - */ - command: Array - /** - * Environment variables to set when running the MCP server - */ - environment?: { - [key: string]: string +export type CompactionPart = { + id: string + sessionID: string + messageID: string + type: "compaction" + auto: boolean +} + +export type Part = + | TextPart + | { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + command?: string + } + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | RetryPart + | CompactionPart + +export type EventMessagePartUpdated = { + type: "message.part.updated" + properties: { + part: Part + delta?: string } - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number } -export type McpOAuthConfig = { - /** - * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. - */ - clientId?: string - /** - * OAuth client secret (if required by the authorization server) - */ - clientSecret?: string - /** - * OAuth scopes to request during authorization - */ - scope?: string +export type EventMessagePartRemoved = { + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string + } } -export type McpRemoteConfig = { - /** - * Type of MCP server connection - */ - type: "remote" - /** - * URL of the remote MCP server - */ - url: string - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Headers to send with the request - */ - headers?: { - [key: string]: string +export type PermissionRequest = { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string } - /** - * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. - */ - oauth?: McpOAuthConfig | false - /** - * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number } -/** - * @deprecated Always uses stretch layout. - */ -export type LayoutConfig = "auto" | "stretch" +export type EventPermissionAsked = { + type: "permission.asked" + properties: PermissionRequest +} -export type Config = { - /** - * JSON schema reference for configuration validation - */ - $schema?: string - /** - * Theme name to use for the interface - */ - theme?: string - keybinds?: KeybindsConfig - logLevel?: LogLevel - /** - * TUI specific settings - */ - tui?: { - /** - * TUI scroll speed - */ - scroll_speed?: number - /** - * Scroll acceleration settings - */ - scroll_acceleration?: { - /** - * Enable scroll acceleration - */ - enabled: boolean - } - /** - * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column - */ - diff_style?: "auto" | "stacked" +export type EventPermissionReplied = { + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" } - server?: ServerConfig - /** - * Command configuration, see https://opencode.ai/docs/commands - */ - command?: { - [key: string]: { - template: string - description?: string - agent?: string - model?: string - subtask?: boolean +} + +export type SessionStatus = + | { + type: "idle" + } + | { + type: "retry" + attempt: number + message: string + next: number + } + | { + type: "busy" } + +export type EventSessionStatus = { + type: "session.status" + properties: { + sessionID: string + status: SessionStatus } - watcher?: { - ignore?: Array +} + +export type EventSessionIdle = { + type: "session.idle" + properties: { + sessionID: string } - plugin?: Array - snapshot?: boolean +} + +export type QuestionOption = { /** - * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing + * Display text (1-5 words, concise) */ - share?: "manual" | "auto" | "disabled" + label: string /** - * @deprecated Use 'share' field instead. Share newly created sessions automatically + * Explanation of choice */ - autoshare?: boolean + description: string +} + +export type QuestionInfo = { /** - * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications + * Complete question */ - autoupdate?: boolean | "notify" + question: string /** - * Disable providers that are loaded automatically + * Very short label (max 12 chars) */ - disabled_providers?: Array + header: string /** - * When set, ONLY these providers will be enabled. All other providers will be ignored + * Available choices */ - enabled_providers?: Array + options: Array /** - * Model to use in the format of provider/model, eg anthropic/claude-2 + * Allow selecting multiple choices */ - model?: string + multiple?: boolean /** - * Small model to use for tasks like title generation in the format of provider/model + * Allow typing a custom answer (default: true) */ - small_model?: string + custom?: boolean +} + +export type QuestionRequest = { + id: string + sessionID: string /** - * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. + * Questions to ask */ - default_agent?: string + questions: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventQuestionAsked = { + type: "question.asked" + properties: QuestionRequest +} + +export type QuestionAnswer = Array + +export type EventQuestionReplied = { + type: "question.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } +} + +export type EventQuestionRejected = { + type: "question.rejected" + properties: { + sessionID: string + requestID: string + } +} + +export type EventSessionCompacted = { + type: "session.compacted" + properties: { + sessionID: string + } +} + +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string + } +} + +export type Todo = { /** - * Custom username to display in conversations instead of system username + * Brief description of the task */ - username?: string + content: string /** - * @deprecated Use `agent` field instead. + * Current status of the task: pending, in_progress, completed, cancelled */ - mode?: { - build?: AgentConfig - plan?: AgentConfig - [key: string]: AgentConfig | undefined + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string + /** + * Unique identifier for the todo item + */ + id: string +} + +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + +export type EventSkillUpdated = { + type: "skill.updated" + properties: { + [key: string]: unknown + } +} + +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number + } +} + +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type EventMcpToolsChanged = { + type: "mcp.tools.changed" + properties: { + server: string + } +} + +export type EventCommandUpdated = { + type: "command.updated" + properties: { + [key: string]: unknown + } +} + +export type EventCommandExecuted = { + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } +} + +export type PermissionAction = "allow" | "deny" | "ask" + +export type PermissionRule = { + permission: string + pattern: string + action: PermissionAction +} + +export type PermissionRuleset = Array + +export type Session = { + id: string + slug: string + projectID: string + directory: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + share?: { + url: string + } + title: string + version: string + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string + } +} + +export type EventSessionCreated = { + type: "session.created" + properties: { + info: Session + } +} + +export type EventSessionUpdated = { + type: "session.updated" + properties: { + info: Session + } +} + +export type EventSessionDeleted = { + type: "session.deleted" + properties: { + info: Session } - /** - * Agent configuration, see https://opencode.ai/docs/agent - */ - agent?: { - plan?: AgentConfig - build?: AgentConfig - general?: AgentConfig - explore?: AgentConfig - title?: AgentConfig - summary?: AgentConfig - compaction?: AgentConfig - [key: string]: AgentConfig | undefined +} + +export type EventSessionDiff = { + type: "session.diff" + properties: { + sessionID: string + diff: Array } - /** - * Custom provider configurations and model overrides - */ - provider?: { - [key: string]: ProviderConfig +} + +export type EventSessionError = { + type: "session.error" + properties: { + sessionID?: string + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError } - /** - * MCP (Model Context Protocol) server configurations - */ - mcp?: { - [key: string]: - | McpLocalConfig - | McpRemoteConfig - | { - enabled: boolean - } +} + +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" + properties: { + branch?: string } - formatter?: - | false - | { - [key: string]: { - disabled?: boolean - command?: Array - environment?: { - [key: string]: string - } - extensions?: Array - } - } - lsp?: - | false - | { - [key: string]: - | { - disabled: true - } - | { - command: Array - extensions?: Array - disabled?: boolean - env?: { - [key: string]: string - } - initialization?: { - [key: string]: unknown - } - } - } - /** - * Additional instruction files or patterns to include - */ - instructions?: Array - layout?: LayoutConfig - permission?: PermissionConfig - tools?: { - [key: string]: boolean +} + +export type Pty = { + id: string + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number +} + +export type EventPtyCreated = { + type: "pty.created" + properties: { + info: Pty } - enterprise?: { - /** - * Enterprise URL - */ - url?: string +} + +export type EventPtyUpdated = { + type: "pty.updated" + properties: { + info: Pty } - compaction?: { - /** - * Enable automatic compaction when context is full (default: true) - */ - auto?: boolean - /** - * Enable pruning of old tool outputs (default: true) - */ - prune?: boolean +} + +export type EventPtyExited = { + type: "pty.exited" + properties: { + id: string + exitCode: number } - experimental?: { - hook?: { - file_edited?: { - [key: string]: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> - } - session_completed?: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> - } - /** - * Number of retries for chat completions on failure - */ - chatMaxRetries?: number - disable_paste_summary?: boolean - /** - * Enable the batch tool - */ - batch_tool?: boolean - /** - * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) - */ - openTelemetry?: boolean - /** - * Tools that should only be available to primary agents. - */ - primary_tools?: Array - /** - * Continue the agent loop when a tool call is denied - */ - continue_loop_on_deny?: boolean - /** - * Timeout in milliseconds for model context protocol (MCP) requests - */ - mcp_timeout?: number +} + +export type EventPtyDeleted = { + type: "pty.deleted" + properties: { + id: string + } +} + +export type EventServerConnected = { + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + +export type Event = + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventProjectUpdated + | EventServerInstanceDisposed + | EventFileWatcherUpdated + | EventConfigUpdated + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventPermissionAsked + | EventPermissionReplied + | EventSessionStatus + | EventSessionIdle + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventSessionCompacted + | EventFileEdited + | EventTodoUpdated + | EventSkillUpdated + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventCommandUpdated + | EventCommandExecuted + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionDiff + | EventSessionError + | EventVcsBranchUpdated + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventServerConnected + | EventGlobalDisposed + +export type GlobalEvent = { + directory: string + payload: Event +} + +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string } } From 616fdc0ef075ba80969bc70ab201ddf87a66669f Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 17:39:02 +0000 Subject: [PATCH 02/27] chore: add planning and memory files for hot-reload feature --- .opencode/agent/planning.md | 31 ++ .../plans/1768342151940-glowing-orchid.md | 30 ++ .opencode/plans/hot-reload-rewrite.md | 301 ++++++++++++++++++ .opencode/remember.jsonc | 16 + 4 files changed, 378 insertions(+) create mode 100644 .opencode/agent/planning.md create mode 100644 .opencode/plans/1768342151940-glowing-orchid.md create mode 100644 .opencode/plans/hot-reload-rewrite.md create mode 100644 .opencode/remember.jsonc diff --git a/.opencode/agent/planning.md b/.opencode/agent/planning.md new file mode 100644 index 00000000000..6c881b1bd66 --- /dev/null +++ b/.opencode/agent/planning.md @@ -0,0 +1,31 @@ +--- +mode: primary +description: Custom planning agent with restricted permissions +color: "#FF5733" +permission: + skill: + "*": "deny" + "plan-prd": "allow" + "plan-tasklist": "allow" + "plan-chat": "allow" + bash: + "*": "deny" + "git log*": "allow" + "git diff*": "allow" + "git show*": "allow" + "git branch*": "allow" + "git status*": "allow" + edit: + "*": "deny" + "planning/*": "allow" +--- + +You are a custom planning agent with restricted permissions. + +You can only: + +- Use specific planning skills (plan-prd, plan-tasklist, plan-chat) +- Run git commands for reading history (log, diff, show, branch, status) +- Edit files in the planning/ directory + +You cannot edit other files or run arbitrary bash commands. diff --git a/.opencode/plans/1768342151940-glowing-orchid.md b/.opencode/plans/1768342151940-glowing-orchid.md new file mode 100644 index 00000000000..185c9794455 --- /dev/null +++ b/.opencode/plans/1768342151940-glowing-orchid.md @@ -0,0 +1,30 @@ +# Plan: Review Upstream PRs + +## Status + +In Progress + +## Overview + +The user wants to review recent Pull Requests (PRs) from the upstream repository (`sst/opencode`) and select the best ones to implement/merge. + +## Implementation Steps + +1. **Identify Upstream**: [Completed] + - Found upstream: `sst/opencode`. +2. **Fetch PRs**: [Completed] + - Retrieved top 10 open PRs. + - Highlights: + - **Features**: SSH management (#8283), Plugin versions (#8279), GitHub URL support (#8263), MCP elicitation (#8243). + - **Fixes**: Memory leaks in TUI (#8255, #8254), Anthropic tool history (#8248), Empty catch blocks (#8271). + - **Docs/Types**: Plan mode restrictions (#8290), ToolContext types (#8269). +3. **Analyze & Select**: + - Present the list to the user. + - **Current Action**: Collaborate to choose the "nicest" ones based on features/fixes. +4. **Plan Implementation**: + - Define how to bring them in (merge, cherry-pick, or re-implement). + +## Verification + +- List of PRs displayed. +- Selection made by user. diff --git a/.opencode/plans/hot-reload-rewrite.md b/.opencode/plans/hot-reload-rewrite.md new file mode 100644 index 00000000000..172caaa84b1 --- /dev/null +++ b/.opencode/plans/hot-reload-rewrite.md @@ -0,0 +1,301 @@ +# Hot-Reload Feature Rewrite Plan + +> **Goal**: Integrate hot-reload for agents/commands/skills/config/tools into upstream, +> keeping changes minimal and properly gated behind experimental flags. + +## Overview + +| Flag | What it enables | Risk Level | +| ------------------------------------------ | --------------------------------- | ---------- | +| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | Watch project root (upstream) | Medium | +| `OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC` | Hot-reload agents/commands/skills | Low-Medium | +| `OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG` | Hot-reload opencode.json/tools | Higher | + +--- + +## Phase 1: Setup & Branch Creation + +- [ ] **1.1** Ensure upstream/dev is fetched and up-to-date +- [ ] **1.2** Create new branch `feature/hot-reload-v2` from `upstream/dev` +- [ ] **1.3** Verify clean state with no uncommitted changes + +--- + +## Phase 2: Flag Definitions + +### File: `packages/opencode/src/flag/flag.ts` + +- [ ] **2.1** Add `OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC` flag +- [ ] **2.2** Add `OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG` flag +- [ ] **2.3** Ensure `OPENCODE_EXPERIMENTAL_FILEWATCHER` is preserved (upstream) + +--- + +## Phase 3: FileWatcher Enhancements + +### File: `packages/opencode/src/file/watcher.ts` + +- [ ] **3.1** Add `Global` import (for context re-entry) +- [ ] **3.2** Add static `require()` statements for parcel watcher bindings (bundler fix) + - darwin-arm64, darwin-x64 + - linux-arm64-glibc, linux-arm64-musl + - linux-x64-glibc, linux-x64-musl + - win32-x64 + - Fallback for other platforms +- [ ] **3.3** Add `Instance.runInContext()` wrapper in subscribe callback + - Capture `directory` before callback + - Re-enter context for Bus.publish to work correctly +- [ ] **3.4** Add config directory watching (gated by `HOT_RELOAD_AGENTIC` OR `HOT_RELOAD_CONFIG`) + - Get directories from `Config.directories()` + - Add `isInsideWatchedPath()` helper to avoid duplicate watches + - Subscribe to each config directory +- [ ] **3.5** Keep upstream's `OPENCODE_EXPERIMENTAL_FILEWATCHER` guard for Instance.directory +- [ ] **3.6** Keep upstream's always-on .git/HEAD watching + +--- + +## Phase 4: Config Hot-Reload + +### File: `packages/opencode/src/config/config.ts` + +- [ ] **4.1** Add Bus import and BusEvent for `config.updated` +- [ ] **4.2** Add FileWatcher import +- [ ] **4.3** Create `Config.Events.Updated` BusEvent definition +- [ ] **4.4** Create `Config.initWatcher()` function + - Subscribe to `FileWatcher.Event.Updated` + - Filter for config file patterns (opencode.json, opencode.jsonc) + - On change: reload config, emit `Config.Events.Updated` + - Guard with `HOT_RELOAD_CONFIG` flag +- [ ] **4.5** Ensure fresh file scanning (no stale glob cache) + +--- + +## Phase 5: Skill Hot-Reload + +### File: `packages/opencode/src/skill/skill.ts` + +- [ ] **5.1** Add Bus, BusEvent, FileWatcher imports +- [ ] **5.2** Create `Skill.Events.Updated` BusEvent definition +- [ ] **5.3** Create `Skill.initWatcher()` function + - Subscribe to `FileWatcher.Event.Updated` + - Filter for skill file patterns (\*.md in skill directories) + - On change: reload skills, emit `Skill.Events.Updated` + - Guard with `HOT_RELOAD_AGENTIC` flag +- [ ] **5.4** Use fresh `fs.readdir` scan instead of cached glob + +--- + +## Phase 6: Agent Hot-Reload + +### File: `packages/opencode/src/agent/agent.ts` + +- [ ] **6.1** Add Bus import +- [ ] **6.2** Create `Agent.initWatcher()` function + - Subscribe to `Config.Events.Updated` + - On config change: agents are automatically refreshed (they read from config) + - Guard with `HOT_RELOAD_AGENTIC` flag + +--- + +## Phase 7: Command Hot-Reload + +### File: `packages/opencode/src/command/index.ts` + +- [ ] **7.1** Add Bus import +- [ ] **7.2** Create `Command.initWatcher()` function + - Subscribe to `Config.Events.Updated` + - On config change: commands are automatically refreshed + - Guard with `HOT_RELOAD_AGENTIC` flag + +--- + +## Phase 8: Tool Registry Hot-Reload + +### File: `packages/opencode/src/tool/registry.ts` + +- [ ] **8.1** Add Bus, Skill imports +- [ ] **8.2** Create `ToolRegistry.initWatcher()` function + - Subscribe to `Config.Events.Updated` and `Skill.Events.Updated` + - On change: bust tool cache, reload tools + - Guard with `HOT_RELOAD_CONFIG` flag for config, `HOT_RELOAD_AGENTIC` for skills + +--- + +## Phase 9: Bootstrap Integration + +### File: `packages/opencode/src/project/bootstrap.ts` + +- [ ] **9.1** Add imports for Config, Skill, Agent, Command, ToolRegistry +- [ ] **9.2** Add conditional `initWatcher()` calls + ```typescript + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC) { + Skill.initWatcher() + Agent.initWatcher() + Command.initWatcher() + } + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG) { + Config.initWatcher() + ToolRegistry.initWatcher() + } + ``` + +--- + +## Phase 10: TUI Event Handlers + +### File: `packages/opencode/src/cli/cmd/tui/context/sync.tsx` + +- [ ] **10.1** Add `config.updated` event handler + - Update config store + - Refresh agents list + - Refresh commands list +- [ ] **10.2** Add `skill.updated` event handler (if applicable) + +### File: `packages/opencode/src/cli/cmd/tui/context/local.tsx` + +- [ ] **10.3** Handle graceful agent removal during active session + - If current agent is removed, fall back gracefully + +--- + +## Phase 11: App (Web) Event Handlers + +### File: `packages/app/src/context/global-sync.tsx` + +- [ ] **11.1** Add `createClient()` helper function (code cleanup) +- [ ] **11.2** Add `config.updated` event handler + - Re-fetch agents, commands, config + - Show toast on error +- [ ] **11.3** Add `skill.updated` event handler + - Re-fetch agents (skills embedded in agent prompts) + - Show toast on error + +--- + +## Phase 12: Markdown Parser Safety + +### File: `packages/opencode/src/config/markdown.ts` + +- [ ] **12.1** Add file access check to handle race conditions + - Check if file exists before parsing + - Handle deleted files gracefully during hot-reload + +--- + +## Phase 13: Bus Event Ordering + +### File: `packages/opencode/src/bus/index.ts` + +- [ ] **13.1** Review event emission timing + - Ensure Bus emits events after subscribers have processed changes + - May need to clone state before emitting to avoid pollution + +--- + +## Phase 14: Tests + +### File: `packages/opencode/test/config/config-delete-hot-reload.test.ts` + +- [ ] **14.1** Port config hot-reload deletion test + - Test that deleted configs are properly detected + - Verify no stale cache issues + +### File: `packages/opencode/test/skill/skill-hot-reload.test.ts` + +- [ ] **14.2** Port skill hot-reload test + - Test skill addition detection + - Test skill modification detection + - Test skill deletion detection + - Configure git user in fixture for robust testing + +### File: `packages/opencode/test/fixture/fixture.ts` + +- [ ] **14.3** Add git user configuration to test fixture + +--- + +## Phase 15: SDK Regeneration + +- [ ] **15.1** Run `./packages/sdk/js/script/build.ts` to regenerate types +- [ ] **15.2** Verify generated types match expected schema + +--- + +## Phase 16: Testing & Verification + +- [ ] **16.1** Run `bun test` in packages/opencode +- [ ] **16.2** Run `bun dev` and manually test hot-reload with flags enabled +- [ ] **16.3** Verify hot-reload works for: + - [ ] Skill files (add/modify/delete) + - [ ] Agent definitions (add/modify/delete) + - [ ] Command definitions (add/modify/delete) + - [ ] opencode.json changes + - [ ] Tool files (add/modify/delete) +- [ ] **16.4** Verify no hot-reload behavior when flags are disabled (default) + +--- + +## Phase 17: Commit & Push + +- [ ] **17.1** Create atomic commits for each logical change: + 1. `feat(flags): add HOT_RELOAD_AGENTIC and HOT_RELOAD_CONFIG flags` + 2. `feat(watcher): extend file watcher for config directory watching` + 3. `feat(config): add hot-reload support for config files` + 4. `feat(skill): add hot-reload support for skill files` + 5. `feat(agent): add hot-reload watcher integration` + 6. `feat(command): add hot-reload watcher integration` + 7. `feat(tools): add hot-reload support for tool registry` + 8. `feat(tui): add event handlers for hot-reload updates` + 9. `feat(app): add event handlers for hot-reload updates` + 10. `test: add hot-reload tests for config and skills` + 11. `chore: regenerate SDK types` +- [ ] **17.2** Push to origin +- [ ] **17.3** Update PR #13 or create new PR + +--- + +## Files Changed Summary + +| File | Changes | +| ---------------------------------------------- | ------------------------------------------------- | +| `flag/flag.ts` | +2 new flags | +| `file/watcher.ts` | Static requires, context fix, config dir watching | +| `config/config.ts` | BusEvent, initWatcher() | +| `skill/skill.ts` | BusEvent, initWatcher() | +| `agent/agent.ts` | initWatcher() | +| `command/index.ts` | initWatcher() | +| `tool/registry.ts` | initWatcher(), cache busting | +| `project/bootstrap.ts` | Conditional initWatcher() calls | +| `config/markdown.ts` | File existence check | +| `bus/index.ts` | Event ordering review | +| `tui/context/sync.tsx` | Event handlers | +| `tui/context/local.tsx` | Graceful agent removal | +| `app/context/global-sync.tsx` | Event handlers, createClient helper | +| `test/config/config-delete-hot-reload.test.ts` | New test | +| `test/skill/skill-hot-reload.test.ts` | New test | +| `test/fixture/fixture.ts` | Git user config | + +--- + +## Risk Assessment + +| Component | Risk | Mitigation | +| ------------------ | --------------------------------- | ------------------------------------ | +| Config hot-reload | High - can break running sessions | Separate flag, careful state cloning | +| Skill hot-reload | Medium - affects prompts | Refresh agents on change | +| Agent hot-reload | Low - just re-reads config | N/A | +| Command hot-reload | Low - just re-reads config | N/A | +| Tool hot-reload | Medium - affects available tools | Cache busting | +| File watcher | Low - upstream already has it | Keep upstream guards | + +--- + +## Success Criteria + +1. ✅ All flags default to OFF (no behavior change for normal users) +2. ✅ When `HOT_RELOAD_AGENTIC=true`: skills/agents/commands hot-reload +3. ✅ When `HOT_RELOAD_CONFIG=true`: opencode.json/tools hot-reload +4. ✅ All existing tests pass +5. ✅ New hot-reload tests pass +6. ✅ No breaking changes to upstream behavior +7. ✅ Minimal diff from upstream/dev diff --git a/.opencode/remember.jsonc b/.opencode/remember.jsonc new file mode 100644 index 00000000000..42d86610f85 --- /dev/null +++ b/.opencode/remember.jsonc @@ -0,0 +1,16 @@ +{ + // Enable or disable the plugin + "enabled": true, + // Where to store memories: "global", "project", or "both" + // - "global": ~/.config/opencode/memory/memories.sqlite (shared across projects) + // - "project": .opencode/memory/memories.sqlite (project-specific) + // - "both": search both, save to project + "scope": "project", + // Memory injection settings + "inject": { + // Number of memories to inject after user messages (default: 5) + "count": 5, + // Score threshold for [important] vs [related] tag (default: 0.6) + "highThreshold": 0.6 + } +} From 6de5f81508072fac2b46cfcab786e48c33820065 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 17:43:04 +0000 Subject: [PATCH 03/27] fix: handle undefined parse result in SkillTool --- packages/opencode/src/tool/skill.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 386abdae745..443f443da2a 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -57,6 +57,8 @@ export const SkillTool = Tool.define("skill", async (ctx) => { }) // Load and parse skill content const parsed = await ConfigMarkdown.parse(skill.location) + if (!parsed) throw new Error(`Failed to parse skill "${params.name}"`) + const dir = path.dirname(skill.location) // Format output similar to plugin pattern From 18e30657ec3ebdce3e3d7cffddc7f6d6d446b984 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 18:05:17 +0000 Subject: [PATCH 04/27] fix: harden hot-reload cache handling --- packages/app/src/context/global-sync.tsx | 3 +- packages/opencode/src/bus/index.ts | 33 +++++++++++-------- .../src/cli/cmd/tui/context/local.tsx | 11 ++++--- .../opencode/src/cli/cmd/tui/context/sync.tsx | 3 +- packages/opencode/src/config/config.ts | 16 +++++---- packages/opencode/src/skill/skill.ts | 8 +++-- 6 files changed, 45 insertions(+), 29 deletions(-) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 7f55912cd09..00c8775e641 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -19,6 +19,7 @@ import { type QuestionRequest, createOpencodeClient, } from "@opencode-ai/sdk/v2/client" +import type { Event } from "@opencode-ai/sdk/v2" import { createStore, produce, reconcile } from "solid-js/store" import { Binary } from "@opencode-ai/util/binary" import { retry } from "@opencode-ai/util/retry" @@ -271,7 +272,7 @@ function createGlobalSync() { const unsub = globalSDK.event.listen((e) => { const directory = e.name - const event = e.details as any + const event = e.details as Event if (directory === "global") { switch (event?.type) { diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 1021c8683b2..7b672d70e6f 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -6,7 +6,7 @@ import { GlobalBus } from "./global" export namespace Bus { const log = Log.create({ service: "bus" }) - type Subscription = (event: any) => void + type Subscription = (event: unknown) => void | Promise export const InstanceDisposed = BusEvent.define( "server.instance.disposed", @@ -17,7 +17,7 @@ export namespace Bus { const state = Instance.state( () => { - const subscriptions = new Map() + const subscriptions = new Map() return { subscriptions, @@ -49,14 +49,20 @@ export namespace Bus { log.info("publishing", { type: def.type, }) - const pending = [] + const pending: Promise[] = [] for (const key of [def.type, "*"]) { const match = state().subscriptions.get(key) for (const sub of match ?? []) { - pending.push(sub(payload)) + const result = sub(payload) + pending.push(Promise.resolve(result)) + } + } + const results = await Promise.allSettled(pending) + for (const result of results) { + if (result.status === "rejected") { + log.error("subscriber failed", { error: result.reason }) } } - await Promise.all(pending) GlobalBus.emit("event", { directory: Instance.directory, payload, @@ -82,24 +88,25 @@ export namespace Bus { }) } - export function subscribeAll(callback: (event: any) => void) { + export function subscribeAll(callback: (event: unknown) => void) { return raw("*", callback) } - function raw(type: string, callback: (event: any) => void) { + function raw(type: string, callback: (event: Event) => void) { log.info("subscribing", { type }) const subscriptions = state().subscriptions - let match = subscriptions.get(type) ?? [] - match.push(callback) + const match = subscriptions.get(type) ?? [] + const wrapped: Subscription = (event) => callback(event as Event) + match.push(wrapped) subscriptions.set(type, match) return () => { log.info("unsubscribing", { type }) - const match = subscriptions.get(type) - if (!match) return - const index = match.indexOf(callback) + const current = subscriptions.get(type) + if (!current) return + const index = current.indexOf(wrapped) if (index === -1) return - match.splice(index, 1) + current.splice(index, 1) } } } diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index c64f2d13436..495596a807f 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -56,12 +56,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ current() { const match = agents().find((x) => x.name === agentStore.current) if (match) return match - // Fallback if current agent was removed (e.g. via hot reload) - if (agents().length > 0) { - setAgentStore("current", agents()[0].name) - return agents()[0] + const fallback = agents()[0] + if (!fallback) { + throw new Error("No agents available") } - return agents()[0]! + // Fallback if current agent was removed (e.g. via hot reload) + setAgentStore("current", fallback.name) + return fallback }, set(name: string) { if (!agents().some((x) => x.name === name)) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 211749aabd2..a5ac0d74209 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -17,6 +17,7 @@ import type { ProviderListResponse, ProviderAuthMethod, VcsInfo, + Event, } from "@opencode-ai/sdk/v2" import { createStore, produce, reconcile } from "solid-js/store" import { useSDK } from "@tui/context/sdk" @@ -105,7 +106,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ const sdk = useSDK() sdk.event.listen((e) => { - const event = e.details as any + const event = e.details as Event switch (event.type) { case "server.instance.disposed": bootstrap() diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 8dc7057f22c..143feda61f5 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -45,7 +45,7 @@ export namespace Config { return merged } - export const state = Instance.state(async () => { + async function initState(): Promise<{ config: Info; directories: string[] }> { const auth = await Auth.all() // Load remote/well-known config first as the base layer (lowest precedence) @@ -59,10 +59,12 @@ export namespace Config { if (!response.ok) { throw new Error(`failed to fetch remote config from ${key}: ${response.status}`) } - const wellknown = (await response.json()) as any - const remoteConfig = wellknown.config ?? {} + const wellknown = (await response.json()) as { config?: unknown } + const remoteConfig = typeof wellknown.config === "object" && wellknown.config !== null ? wellknown.config : {} // Add $schema to prevent load() from trying to write back to a non-existent file - if (!remoteConfig.$schema) remoteConfig.$schema = "https://opencode.ai/config.json" + if (typeof (remoteConfig as { $schema?: string }).$schema !== "string") { + ;(remoteConfig as { $schema?: string }).$schema = "https://opencode.ai/config.json" + } result = mergeConfigConcatArrays( result, await load(JSON.stringify(remoteConfig), `${key}/.well-known/opencode`), @@ -194,7 +196,9 @@ export namespace Config { config: result, directories, } - }) + } + + export const state = Instance.state(initState) export async function installDependencies(dir: string) { const pkg = path.join(dir, "package.json") @@ -1246,7 +1250,7 @@ export namespace Config { log.info("config file changed, reloading", { file: event.properties.file, event: event.properties.event }) try { - // Invalidate and reload state + await Instance.invalidate(initState) const result = await state() Bus.publish(Events.Updated, result.config) log.info("config reloaded successfully") diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 9b5f275d4cb..7a2dd8e651d 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -47,7 +47,7 @@ export namespace Skill { const OPENCODE_SKILL_GLOB = new Bun.Glob("{skill,skills}/**/SKILL.md") const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md") - export const state = Instance.state(async () => { + async function initState(): Promise> { const skills: Record = {} const addSkill = async (match: string) => { @@ -123,7 +123,9 @@ export namespace Skill { } return skills - }) + } + + export const state = Instance.state(initState) export async function get(name: string) { return state().then((x) => x[name]) @@ -148,7 +150,7 @@ export namespace Skill { log.info("skill file changed, reloading", { file: event.properties.file, event: event.properties.event }) try { - // Reload skills and emit update event + await Instance.invalidate(initState) await state() Bus.publish(Events.Updated, {}) log.info("skills reloaded successfully") From 2c95b58dcc2115dcf56ba6bb2c4e3139964adea6 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 18:08:09 +0000 Subject: [PATCH 05/27] fix: restore typed bus events --- packages/opencode/src/bus/index.ts | 2 +- packages/opencode/src/plugin/index.ts | 3 ++- packages/opencode/src/server/server.ts | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/bus/index.ts b/packages/opencode/src/bus/index.ts index 7b672d70e6f..29cb4744b02 100644 --- a/packages/opencode/src/bus/index.ts +++ b/packages/opencode/src/bus/index.ts @@ -88,7 +88,7 @@ export namespace Bus { }) } - export function subscribeAll(callback: (event: unknown) => void) { + export function subscribeAll(callback: (event: Event) => void) { return raw("*", callback) } diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts index 84de520b81d..85497b5ebe1 100644 --- a/packages/opencode/src/plugin/index.ts +++ b/packages/opencode/src/plugin/index.ts @@ -3,6 +3,7 @@ import { Config } from "../config/config" import { Bus } from "../bus" import { Log } from "../util/log" import { createOpencodeClient } from "@opencode-ai/sdk" +import type { Event } from "@opencode-ai/sdk" import { Server } from "../server/server" import { BunProc } from "../bun" import { Instance } from "../project/instance" @@ -123,7 +124,7 @@ export namespace Plugin { // @ts-expect-error this is because we haven't moved plugin to sdk v2 await hook.config?.(config) } - Bus.subscribeAll(async (input) => { + Bus.subscribeAll(async (input) => { const hooks = await state().then((x) => x.hooks) for (const hook of hooks) { hook["event"]?.({ diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 7015c818822..c8d4a4e29bb 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -9,6 +9,7 @@ import { stream, streamSSE } from "hono/streaming" import { proxy } from "hono/proxy" import { basicAuth } from "hono/basic-auth" import { Session } from "../session" +import type { Event } from "@opencode-ai/sdk/v2" import z from "zod" import { Provider } from "../provider/provider" import { filter, mapValues, sortBy, pipe } from "remeda" @@ -2802,7 +2803,7 @@ export namespace Server { properties: {}, }), }) - const unsub = Bus.subscribeAll(async (event) => { + const unsub = Bus.subscribeAll(async (event) => { await stream.writeSSE({ data: JSON.stringify(event), }) From a8d9145d847024c8b7976b61f1a67ac4bbbdb608 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 18:18:02 +0000 Subject: [PATCH 06/27] chore: trim hot-reload noise --- .opencode/agent/planning.md | 31 -- .../plans/1768342151940-glowing-orchid.md | 30 -- .opencode/plans/hot-reload-rewrite.md | 301 ------------------ .opencode/remember.jsonc | 16 - packages/app/src/context/global-sync.tsx | 21 +- packages/opencode/src/agent/agent.ts | 20 -- .../src/cli/cmd/tui/context/local.tsx | 1 - .../opencode/src/cli/cmd/tui/context/sync.tsx | 1 - packages/opencode/src/command/index.ts | 20 +- packages/opencode/src/config/config.ts | 18 +- packages/opencode/src/config/markdown.ts | 4 +- packages/opencode/src/file/watcher.ts | 7 - packages/opencode/src/flag/flag.ts | 1 - packages/opencode/src/project/bootstrap.ts | 2 - packages/opencode/src/project/instance.ts | 10 - packages/opencode/src/skill/skill.ts | 18 +- packages/opencode/src/tool/registry.ts | 7 - 17 files changed, 12 insertions(+), 496 deletions(-) delete mode 100644 .opencode/agent/planning.md delete mode 100644 .opencode/plans/1768342151940-glowing-orchid.md delete mode 100644 .opencode/plans/hot-reload-rewrite.md delete mode 100644 .opencode/remember.jsonc diff --git a/.opencode/agent/planning.md b/.opencode/agent/planning.md deleted file mode 100644 index 6c881b1bd66..00000000000 --- a/.opencode/agent/planning.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -mode: primary -description: Custom planning agent with restricted permissions -color: "#FF5733" -permission: - skill: - "*": "deny" - "plan-prd": "allow" - "plan-tasklist": "allow" - "plan-chat": "allow" - bash: - "*": "deny" - "git log*": "allow" - "git diff*": "allow" - "git show*": "allow" - "git branch*": "allow" - "git status*": "allow" - edit: - "*": "deny" - "planning/*": "allow" ---- - -You are a custom planning agent with restricted permissions. - -You can only: - -- Use specific planning skills (plan-prd, plan-tasklist, plan-chat) -- Run git commands for reading history (log, diff, show, branch, status) -- Edit files in the planning/ directory - -You cannot edit other files or run arbitrary bash commands. diff --git a/.opencode/plans/1768342151940-glowing-orchid.md b/.opencode/plans/1768342151940-glowing-orchid.md deleted file mode 100644 index 185c9794455..00000000000 --- a/.opencode/plans/1768342151940-glowing-orchid.md +++ /dev/null @@ -1,30 +0,0 @@ -# Plan: Review Upstream PRs - -## Status - -In Progress - -## Overview - -The user wants to review recent Pull Requests (PRs) from the upstream repository (`sst/opencode`) and select the best ones to implement/merge. - -## Implementation Steps - -1. **Identify Upstream**: [Completed] - - Found upstream: `sst/opencode`. -2. **Fetch PRs**: [Completed] - - Retrieved top 10 open PRs. - - Highlights: - - **Features**: SSH management (#8283), Plugin versions (#8279), GitHub URL support (#8263), MCP elicitation (#8243). - - **Fixes**: Memory leaks in TUI (#8255, #8254), Anthropic tool history (#8248), Empty catch blocks (#8271). - - **Docs/Types**: Plan mode restrictions (#8290), ToolContext types (#8269). -3. **Analyze & Select**: - - Present the list to the user. - - **Current Action**: Collaborate to choose the "nicest" ones based on features/fixes. -4. **Plan Implementation**: - - Define how to bring them in (merge, cherry-pick, or re-implement). - -## Verification - -- List of PRs displayed. -- Selection made by user. diff --git a/.opencode/plans/hot-reload-rewrite.md b/.opencode/plans/hot-reload-rewrite.md deleted file mode 100644 index 172caaa84b1..00000000000 --- a/.opencode/plans/hot-reload-rewrite.md +++ /dev/null @@ -1,301 +0,0 @@ -# Hot-Reload Feature Rewrite Plan - -> **Goal**: Integrate hot-reload for agents/commands/skills/config/tools into upstream, -> keeping changes minimal and properly gated behind experimental flags. - -## Overview - -| Flag | What it enables | Risk Level | -| ------------------------------------------ | --------------------------------- | ---------- | -| `OPENCODE_EXPERIMENTAL_FILEWATCHER` | Watch project root (upstream) | Medium | -| `OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC` | Hot-reload agents/commands/skills | Low-Medium | -| `OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG` | Hot-reload opencode.json/tools | Higher | - ---- - -## Phase 1: Setup & Branch Creation - -- [ ] **1.1** Ensure upstream/dev is fetched and up-to-date -- [ ] **1.2** Create new branch `feature/hot-reload-v2` from `upstream/dev` -- [ ] **1.3** Verify clean state with no uncommitted changes - ---- - -## Phase 2: Flag Definitions - -### File: `packages/opencode/src/flag/flag.ts` - -- [ ] **2.1** Add `OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC` flag -- [ ] **2.2** Add `OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG` flag -- [ ] **2.3** Ensure `OPENCODE_EXPERIMENTAL_FILEWATCHER` is preserved (upstream) - ---- - -## Phase 3: FileWatcher Enhancements - -### File: `packages/opencode/src/file/watcher.ts` - -- [ ] **3.1** Add `Global` import (for context re-entry) -- [ ] **3.2** Add static `require()` statements for parcel watcher bindings (bundler fix) - - darwin-arm64, darwin-x64 - - linux-arm64-glibc, linux-arm64-musl - - linux-x64-glibc, linux-x64-musl - - win32-x64 - - Fallback for other platforms -- [ ] **3.3** Add `Instance.runInContext()` wrapper in subscribe callback - - Capture `directory` before callback - - Re-enter context for Bus.publish to work correctly -- [ ] **3.4** Add config directory watching (gated by `HOT_RELOAD_AGENTIC` OR `HOT_RELOAD_CONFIG`) - - Get directories from `Config.directories()` - - Add `isInsideWatchedPath()` helper to avoid duplicate watches - - Subscribe to each config directory -- [ ] **3.5** Keep upstream's `OPENCODE_EXPERIMENTAL_FILEWATCHER` guard for Instance.directory -- [ ] **3.6** Keep upstream's always-on .git/HEAD watching - ---- - -## Phase 4: Config Hot-Reload - -### File: `packages/opencode/src/config/config.ts` - -- [ ] **4.1** Add Bus import and BusEvent for `config.updated` -- [ ] **4.2** Add FileWatcher import -- [ ] **4.3** Create `Config.Events.Updated` BusEvent definition -- [ ] **4.4** Create `Config.initWatcher()` function - - Subscribe to `FileWatcher.Event.Updated` - - Filter for config file patterns (opencode.json, opencode.jsonc) - - On change: reload config, emit `Config.Events.Updated` - - Guard with `HOT_RELOAD_CONFIG` flag -- [ ] **4.5** Ensure fresh file scanning (no stale glob cache) - ---- - -## Phase 5: Skill Hot-Reload - -### File: `packages/opencode/src/skill/skill.ts` - -- [ ] **5.1** Add Bus, BusEvent, FileWatcher imports -- [ ] **5.2** Create `Skill.Events.Updated` BusEvent definition -- [ ] **5.3** Create `Skill.initWatcher()` function - - Subscribe to `FileWatcher.Event.Updated` - - Filter for skill file patterns (\*.md in skill directories) - - On change: reload skills, emit `Skill.Events.Updated` - - Guard with `HOT_RELOAD_AGENTIC` flag -- [ ] **5.4** Use fresh `fs.readdir` scan instead of cached glob - ---- - -## Phase 6: Agent Hot-Reload - -### File: `packages/opencode/src/agent/agent.ts` - -- [ ] **6.1** Add Bus import -- [ ] **6.2** Create `Agent.initWatcher()` function - - Subscribe to `Config.Events.Updated` - - On config change: agents are automatically refreshed (they read from config) - - Guard with `HOT_RELOAD_AGENTIC` flag - ---- - -## Phase 7: Command Hot-Reload - -### File: `packages/opencode/src/command/index.ts` - -- [ ] **7.1** Add Bus import -- [ ] **7.2** Create `Command.initWatcher()` function - - Subscribe to `Config.Events.Updated` - - On config change: commands are automatically refreshed - - Guard with `HOT_RELOAD_AGENTIC` flag - ---- - -## Phase 8: Tool Registry Hot-Reload - -### File: `packages/opencode/src/tool/registry.ts` - -- [ ] **8.1** Add Bus, Skill imports -- [ ] **8.2** Create `ToolRegistry.initWatcher()` function - - Subscribe to `Config.Events.Updated` and `Skill.Events.Updated` - - On change: bust tool cache, reload tools - - Guard with `HOT_RELOAD_CONFIG` flag for config, `HOT_RELOAD_AGENTIC` for skills - ---- - -## Phase 9: Bootstrap Integration - -### File: `packages/opencode/src/project/bootstrap.ts` - -- [ ] **9.1** Add imports for Config, Skill, Agent, Command, ToolRegistry -- [ ] **9.2** Add conditional `initWatcher()` calls - ```typescript - if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC) { - Skill.initWatcher() - Agent.initWatcher() - Command.initWatcher() - } - if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG) { - Config.initWatcher() - ToolRegistry.initWatcher() - } - ``` - ---- - -## Phase 10: TUI Event Handlers - -### File: `packages/opencode/src/cli/cmd/tui/context/sync.tsx` - -- [ ] **10.1** Add `config.updated` event handler - - Update config store - - Refresh agents list - - Refresh commands list -- [ ] **10.2** Add `skill.updated` event handler (if applicable) - -### File: `packages/opencode/src/cli/cmd/tui/context/local.tsx` - -- [ ] **10.3** Handle graceful agent removal during active session - - If current agent is removed, fall back gracefully - ---- - -## Phase 11: App (Web) Event Handlers - -### File: `packages/app/src/context/global-sync.tsx` - -- [ ] **11.1** Add `createClient()` helper function (code cleanup) -- [ ] **11.2** Add `config.updated` event handler - - Re-fetch agents, commands, config - - Show toast on error -- [ ] **11.3** Add `skill.updated` event handler - - Re-fetch agents (skills embedded in agent prompts) - - Show toast on error - ---- - -## Phase 12: Markdown Parser Safety - -### File: `packages/opencode/src/config/markdown.ts` - -- [ ] **12.1** Add file access check to handle race conditions - - Check if file exists before parsing - - Handle deleted files gracefully during hot-reload - ---- - -## Phase 13: Bus Event Ordering - -### File: `packages/opencode/src/bus/index.ts` - -- [ ] **13.1** Review event emission timing - - Ensure Bus emits events after subscribers have processed changes - - May need to clone state before emitting to avoid pollution - ---- - -## Phase 14: Tests - -### File: `packages/opencode/test/config/config-delete-hot-reload.test.ts` - -- [ ] **14.1** Port config hot-reload deletion test - - Test that deleted configs are properly detected - - Verify no stale cache issues - -### File: `packages/opencode/test/skill/skill-hot-reload.test.ts` - -- [ ] **14.2** Port skill hot-reload test - - Test skill addition detection - - Test skill modification detection - - Test skill deletion detection - - Configure git user in fixture for robust testing - -### File: `packages/opencode/test/fixture/fixture.ts` - -- [ ] **14.3** Add git user configuration to test fixture - ---- - -## Phase 15: SDK Regeneration - -- [ ] **15.1** Run `./packages/sdk/js/script/build.ts` to regenerate types -- [ ] **15.2** Verify generated types match expected schema - ---- - -## Phase 16: Testing & Verification - -- [ ] **16.1** Run `bun test` in packages/opencode -- [ ] **16.2** Run `bun dev` and manually test hot-reload with flags enabled -- [ ] **16.3** Verify hot-reload works for: - - [ ] Skill files (add/modify/delete) - - [ ] Agent definitions (add/modify/delete) - - [ ] Command definitions (add/modify/delete) - - [ ] opencode.json changes - - [ ] Tool files (add/modify/delete) -- [ ] **16.4** Verify no hot-reload behavior when flags are disabled (default) - ---- - -## Phase 17: Commit & Push - -- [ ] **17.1** Create atomic commits for each logical change: - 1. `feat(flags): add HOT_RELOAD_AGENTIC and HOT_RELOAD_CONFIG flags` - 2. `feat(watcher): extend file watcher for config directory watching` - 3. `feat(config): add hot-reload support for config files` - 4. `feat(skill): add hot-reload support for skill files` - 5. `feat(agent): add hot-reload watcher integration` - 6. `feat(command): add hot-reload watcher integration` - 7. `feat(tools): add hot-reload support for tool registry` - 8. `feat(tui): add event handlers for hot-reload updates` - 9. `feat(app): add event handlers for hot-reload updates` - 10. `test: add hot-reload tests for config and skills` - 11. `chore: regenerate SDK types` -- [ ] **17.2** Push to origin -- [ ] **17.3** Update PR #13 or create new PR - ---- - -## Files Changed Summary - -| File | Changes | -| ---------------------------------------------- | ------------------------------------------------- | -| `flag/flag.ts` | +2 new flags | -| `file/watcher.ts` | Static requires, context fix, config dir watching | -| `config/config.ts` | BusEvent, initWatcher() | -| `skill/skill.ts` | BusEvent, initWatcher() | -| `agent/agent.ts` | initWatcher() | -| `command/index.ts` | initWatcher() | -| `tool/registry.ts` | initWatcher(), cache busting | -| `project/bootstrap.ts` | Conditional initWatcher() calls | -| `config/markdown.ts` | File existence check | -| `bus/index.ts` | Event ordering review | -| `tui/context/sync.tsx` | Event handlers | -| `tui/context/local.tsx` | Graceful agent removal | -| `app/context/global-sync.tsx` | Event handlers, createClient helper | -| `test/config/config-delete-hot-reload.test.ts` | New test | -| `test/skill/skill-hot-reload.test.ts` | New test | -| `test/fixture/fixture.ts` | Git user config | - ---- - -## Risk Assessment - -| Component | Risk | Mitigation | -| ------------------ | --------------------------------- | ------------------------------------ | -| Config hot-reload | High - can break running sessions | Separate flag, careful state cloning | -| Skill hot-reload | Medium - affects prompts | Refresh agents on change | -| Agent hot-reload | Low - just re-reads config | N/A | -| Command hot-reload | Low - just re-reads config | N/A | -| Tool hot-reload | Medium - affects available tools | Cache busting | -| File watcher | Low - upstream already has it | Keep upstream guards | - ---- - -## Success Criteria - -1. ✅ All flags default to OFF (no behavior change for normal users) -2. ✅ When `HOT_RELOAD_AGENTIC=true`: skills/agents/commands hot-reload -3. ✅ When `HOT_RELOAD_CONFIG=true`: opencode.json/tools hot-reload -4. ✅ All existing tests pass -5. ✅ New hot-reload tests pass -6. ✅ No breaking changes to upstream behavior -7. ✅ Minimal diff from upstream/dev diff --git a/.opencode/remember.jsonc b/.opencode/remember.jsonc deleted file mode 100644 index 42d86610f85..00000000000 --- a/.opencode/remember.jsonc +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Enable or disable the plugin - "enabled": true, - // Where to store memories: "global", "project", or "both" - // - "global": ~/.config/opencode/memory/memories.sqlite (shared across projects) - // - "project": .opencode/memory/memories.sqlite (project-specific) - // - "both": search both, save to project - "scope": "project", - // Memory injection settings - "inject": { - // Number of memories to inject after user messages (default: 5) - "count": 5, - // Score threshold for [important] vs [related] tag (default: 0.6) - "highThreshold": 0.6 - } -} diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index 00c8775e641..ef6bb21733b 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -500,29 +500,12 @@ function createGlobalSync() { sdk.app.agents().then((x) => setStore("agent", x.data ?? [])), sdk.command.list().then((x) => setStore("command", x.data ?? [])), sdk.config.get().then((x) => setStore("config", x.data!)), - ]).catch((err) => { - console.error("Failed to refresh config-related data:", err) - showToast({ - title: "Config Refresh Failed", - description: "Some updates may not be reflected. Try reloading.", - variant: "error", - }) - }) + ]) break } case "skill.updated": { const sdk = createClient(directory) - sdk.app - .agents() - .then((x) => setStore("agent", x.data ?? [])) - .catch((err) => { - console.error("Failed to reload agents after skill update:", err) - showToast({ - title: "Agent Refresh Failed", - description: "Could not update agents after a skill change.", - variant: "error", - }) - }) + sdk.app.agents().then((x) => setStore("agent", x.data ?? [])) break } } diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index c457b80ec2a..64875091916 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -5,9 +5,6 @@ import { generateObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" import { Truncate } from "../tool/truncation" -import { Bus } from "@/bus" -import { Flag } from "@/flag/flag" -import { Log } from "@/util/log" import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" @@ -20,8 +17,6 @@ import { Global } from "@/global" import path from "path" export namespace Agent { - const log = Log.create({ service: "agent" }) - export const Info = z .object({ name: z.string(), @@ -300,19 +295,4 @@ export namespace Agent { }) return result.object } - - /** - * Initialize agent hot-reload watcher. - * Agents are loaded from config, so we listen for config updates. - * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC flag. - */ - export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return - - Bus.subscribe(Config.Events.Updated, async () => { - log.info("config updated, agents will be refreshed on next access") - // Agents are loaded lazily from Config, so nothing to do here - // The next call to Agent.get() or Agent.list() will get fresh data - }) - } } diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 495596a807f..aa68db49404 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -60,7 +60,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (!fallback) { throw new Error("No agents available") } - // Fallback if current agent was removed (e.g. via hot reload) setAgentStore("current", fallback.name) return fallback }, diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index a5ac0d74209..d4978a76319 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -307,7 +307,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ } case "config.updated": { - // @ts-ignore - type will be generated setStore("config", reconcile(event.properties)) sdk.client.app.agents().then((x) => setStore("agent", reconcile(x.data ?? []))) sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))) diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 336480b92b3..d94fe91b99c 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -8,11 +8,8 @@ import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" import { MCP } from "../mcp" import { Flag } from "@/flag/flag" -import { Log } from "@/util/log" export namespace Command { - const log = Log.create({ service: "command" }) - export const Event = { Updated: BusEvent.define("command.updated", z.object({})), Executed: BusEvent.define( @@ -137,24 +134,13 @@ export namespace Command { return state().then((x) => Object.values(x)) } - /** - * Initialize command hot-reload watcher. - * Commands are loaded from config, so we listen for config updates. - * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC flag. - */ export function initWatcher() { if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return Bus.subscribe(Config.Events.Updated, async () => { - log.info("config updated, reloading commands") - try { - await Instance.invalidate(initState) - await list() // Force reload - Bus.publish(Event.Updated, {}) - log.info("commands reloaded successfully") - } catch (err) { - log.error("failed to reload commands", { error: err }) - } + await Instance.invalidate(initState) + await list() + Bus.publish(Event.Updated, {}) }) } } diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 143feda61f5..f0644364da8 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1233,11 +1233,6 @@ export namespace Config { return state().then((x) => x.directories) } - /** - * Initialize config hot-reload watcher. - * Listens for file changes in config directories and reloads config when opencode.json changes. - * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG flag. - */ export function initWatcher() { if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) return @@ -1247,16 +1242,9 @@ export namespace Config { const filename = path.basename(event.properties.file) if (!configPatterns.includes(filename)) return - log.info("config file changed, reloading", { file: event.properties.file, event: event.properties.event }) - - try { - await Instance.invalidate(initState) - const result = await state() - Bus.publish(Events.Updated, result.config) - log.info("config reloaded successfully") - } catch (err) { - log.error("failed to reload config", { error: err }) - } + await Instance.invalidate(initState) + const result = await state() + Bus.publish(Events.Updated, result.config) }) } } diff --git a/packages/opencode/src/config/markdown.ts b/packages/opencode/src/config/markdown.ts index a1813a949f0..f20842c41a9 100644 --- a/packages/opencode/src/config/markdown.ts +++ b/packages/opencode/src/config/markdown.ts @@ -15,9 +15,7 @@ export namespace ConfigMarkdown { } export async function parse(filePath: string) { - const file = Bun.file(filePath) - if (!(await file.exists())) return undefined - const template = await file.text() + const template = await Bun.file(filePath).text() try { const md = matter(template) diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index ef5bcdcc349..98666173381 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -35,7 +35,6 @@ export namespace FileWatcher { const watcher = lazy(() => { const libc = typeof OPENCODE_LIBC === "string" && OPENCODE_LIBC.length > 0 ? OPENCODE_LIBC : "glibc" - // Use static requires for bundler compatibility let binding if (process.platform === "darwin" && process.arch === "arm64") { binding = require("@parcel/watcher-darwin-arm64") @@ -75,11 +74,9 @@ export namespace FileWatcher { } log.info("watcher backend", { platform: process.platform, backend }) - // Capture directory now - callback runs outside AsyncLocalStorage context const directory = Instance.directory const subscribe: ParcelWatcher.SubscribeCallback = (err, evts) => { if (err) return - // Re-enter Instance context for Bus.publish to work correctly Instance.runInContext(directory, () => { for (const evt of evts) { if (evt.type === "create") Bus.publish(Event.Updated, { file: evt.path, event: "add" }) @@ -93,7 +90,6 @@ export namespace FileWatcher { const cfgIgnores = cfg.watcher?.ignore ?? [] const watchedPaths = new Set() - // Helper to check if path is already inside a watched directory const isInsideWatchedPath = (targetPath: string) => { const normalizedTarget = path.resolve(targetPath) for (const watched of watchedPaths) { @@ -105,7 +101,6 @@ export namespace FileWatcher { return false } - // 1. Watch Instance.directory (gated by upstream flag) if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) { const pending = watcher().subscribe(Instance.directory, subscribe, { ignore: [...FileIgnore.PATTERNS, ...cfgIgnores], @@ -123,7 +118,6 @@ export namespace FileWatcher { } } - // 2. Watch .git directory for HEAD changes (always on for git projects) const vcsDir = await $`git rev-parse --git-dir` .quiet() .nothrow() @@ -150,7 +144,6 @@ export namespace FileWatcher { } } - // 3. Watch config directories for hot-reload (gated by hot-reload flags) if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC() || Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) { const configDirectories = await Config.directories() for (const dir of configDirectories) { diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index e1c420570d7..7732a2a66be 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -40,7 +40,6 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL") export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") - // Dynamic getters for hot reload to support testing overrides export const OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC = () => truthy("OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC") export const OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG = () => truthy("OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG") diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 6c009c754e9..ae6ed69b79e 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -14,7 +14,6 @@ import { ShareNext } from "@/share/share-next" import { Config } from "../config/config" import { Skill } from "../skill/skill" import { ToolRegistry } from "@/tool/registry" -import { Agent } from "@/agent/agent" import { Flag } from "@/flag/flag" export async function InstanceBootstrap() { @@ -27,7 +26,6 @@ export async function InstanceBootstrap() { FileWatcher.init() if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { Skill.initWatcher() - Agent.initWatcher() Command.initWatcher() } if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) { diff --git a/packages/opencode/src/project/instance.ts b/packages/opencode/src/project/instance.ts index e151864c789..810a1216609 100644 --- a/packages/opencode/src/project/instance.ts +++ b/packages/opencode/src/project/instance.ts @@ -6,9 +6,6 @@ import { iife } from "@/util/iife" import { GlobalBus } from "@/bus/global" import { Filesystem } from "@/util/filesystem" -const MODULE_ID = Math.random() -console.log("DEBUG: Instance module loaded", MODULE_ID) - interface Context { directory: string worktree: string @@ -16,7 +13,6 @@ interface Context { } const context = Context.create("instance") const cache = new Map>() -// Resolved contexts for synchronous access (e.g., from native callbacks) const resolved = new Map() export const Instance = { @@ -34,7 +30,6 @@ export const Instance = { await context.provide(ctx, async () => { await input.init?.() }) - // Store resolved context for synchronous access resolved.set(input.directory, ctx) return ctx }) @@ -100,11 +95,6 @@ export const Instance = { cache.clear() resolved.clear() }, - /** - * Run a function within an existing instance context synchronously. - * The instance must already be fully initialized (created via provide()). - * Returns undefined if the instance doesn't exist or isn't ready. - */ runInContext(directory: string, fn: () => R): R | undefined { const ctx = resolved.get(directory) if (!ctx) return undefined diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 7a2dd8e651d..8fa01522127 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -135,11 +135,6 @@ export namespace Skill { return state().then((x) => Object.values(x)) } - /** - * Initialize skill hot-reload watcher. - * Listens for file changes in skill directories and reloads skills when SKILL.md files change. - * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC flag. - */ export function initWatcher() { if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return @@ -147,16 +142,9 @@ export namespace Skill { const filename = path.basename(event.properties.file) if (filename !== "SKILL.md") return - log.info("skill file changed, reloading", { file: event.properties.file, event: event.properties.event }) - - try { - await Instance.invalidate(initState) - await state() - Bus.publish(Events.Updated, {}) - log.info("skills reloaded successfully") - } catch (err) { - log.error("failed to reload skills", { error: err }) - } + await Instance.invalidate(initState) + await state() + Bus.publish(Events.Updated, {}) }) } } diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 1fa2168f0bd..5055a84eff8 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -36,7 +36,6 @@ export namespace ToolRegistry { const custom = [] as Tool.Info[] const glob = new Bun.Glob("{tool,tools}/*.{js,ts}") - // Cache busting for hot reload const cacheBust = Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG() || Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC() ? `?t=${Date.now()}` @@ -151,16 +150,10 @@ export namespace ToolRegistry { return result } - /** - * Initialize tool registry hot-reload watcher. - * Reloads tools when config changes (to pick up new tool files or config directories). - * Gated by OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG flag. - */ export function initWatcher() { if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) return Bus.subscribe(Config.Events.Updated, async () => { - log.info("config updated, reloading tools") await Instance.invalidate(initState) }) } From db795693c3b71f8008550238e0cfc23220748ad4 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 18:18:41 +0000 Subject: [PATCH 07/27] test: trim hot-reload setup --- packages/opencode/test/config/config-delete-hot-reload.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/opencode/test/config/config-delete-hot-reload.test.ts b/packages/opencode/test/config/config-delete-hot-reload.test.ts index 45fa2fd7885..16e975e111b 100644 --- a/packages/opencode/test/config/config-delete-hot-reload.test.ts +++ b/packages/opencode/test/config/config-delete-hot-reload.test.ts @@ -25,8 +25,6 @@ async function waitForWatcherReady(root: string, label: string) { const readyPath = path.join(root, ".opencode", `watcher-ready-${label}.txt`) const readyEvent = new Promise((resolve) => { const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, (evt) => { - console.log("Event received:", evt.properties.file, evt.properties.event) - console.log("Expecting:", readyPath) if (evt.properties.file.replaceAll("\\", "/") !== readyPath.replaceAll("\\", "/")) return unsubscribe() resolve() @@ -75,7 +73,6 @@ Run $ARGUMENTS`, init: async () => { FileWatcher.init() Config.initWatcher() - Agent.initWatcher() Command.initWatcher() await new Promise((resolve) => setTimeout(resolve, 200)) }, From aa0b69153f893f0edf51c873fc35bd782e85f061 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 18:26:55 +0000 Subject: [PATCH 08/27] fix: restore hot-reload watchers --- packages/opencode/src/config/config.ts | 29 ++++++++-- packages/opencode/src/file/watcher.ts | 77 +++++++++++++------------- packages/opencode/src/skill/skill.ts | 39 +++++++++++-- 3 files changed, 95 insertions(+), 50 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index f0644364da8..ea6063365ac 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1236,15 +1236,32 @@ export namespace Config { export function initWatcher() { if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) return - const configPatterns = ["opencode.json", "opencode.jsonc"] - Bus.subscribe(FileWatcher.Event.Updated, async (event) => { - const filename = path.basename(event.properties.file) - if (!configPatterns.includes(filename)) return + const filepath = event.properties.file.replaceAll("\\", "/") + const isConfigFile = filepath.endsWith("opencode.json") || filepath.endsWith("opencode.jsonc") + const isConfigExtension = filepath.endsWith(".json") || filepath.endsWith(".jsonc") || filepath.endsWith(".md") + const isUnlink = event.properties.event === "unlink" + + const configRoot = Global.Path.config.replaceAll("\\", "/") + const configDirs = await directories() + const normalizedDirs = configDirs.map((dir) => dir.replaceAll("\\", "/")) + const looksLikeConfigDir = + filepath.includes("/.opencode/") || + filepath.startsWith(".opencode/") || + filepath.includes("/.config/opencode/") || + filepath.startsWith(".config/opencode/") + const inConfigDir = + looksLikeConfigDir || + filepath === configRoot || + filepath.startsWith(configRoot + "/") || + normalizedDirs.some((dir) => Filesystem.contains(dir, filepath) || filepath === dir) + + const shouldInvalidate = isConfigFile || (inConfigDir && (isConfigExtension || isUnlink)) + if (!shouldInvalidate) return await Instance.invalidate(initState) - const result = await state() - Bus.publish(Events.Updated, result.config) + const cfg = await get() + Bus.publish(Events.Updated, cfg) }) } } diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 98666173381..4fab7e293f2 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -60,7 +60,6 @@ export namespace FileWatcher { const state = Instance.state( async () => { - if (Instance.project.vcs !== "git") return {} log.info("init") const cfg = await Config.get() const backend = (() => { @@ -101,46 +100,46 @@ export namespace FileWatcher { return false } - if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) { - const pending = watcher().subscribe(Instance.directory, subscribe, { - ignore: [...FileIgnore.PATTERNS, ...cfgIgnores], - backend, - }) - const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => { - log.error("failed to subscribe to Instance.directory", { error: err }) - pending.then((s) => s.unsubscribe()).catch(() => {}) - return undefined - }) - if (sub) { - subs.push(sub) - watchedPaths.add(Instance.directory) - log.info("watching", { path: Instance.directory }) - } + const pending = watcher().subscribe(Instance.directory, subscribe, { + ignore: [...FileIgnore.PATTERNS, ...cfgIgnores], + backend, + }) + const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => { + log.error("failed to subscribe", { path: Instance.directory, error: err }) + pending.then((s) => s.unsubscribe()).catch(() => {}) + return undefined + }) + if (sub) { + subs.push(sub) + watchedPaths.add(Instance.directory) + log.info("watching", { path: Instance.directory }) } - const vcsDir = await $`git rev-parse --git-dir` - .quiet() - .nothrow() - .cwd(Instance.worktree) - .text() - .then((x) => path.resolve(Instance.worktree, x.trim())) - .catch(() => undefined) - if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) { - const gitDirContents = await readdir(vcsDir).catch(() => []) - const ignoreList = gitDirContents.filter((entry) => entry !== "HEAD") - const pending = watcher().subscribe(vcsDir, subscribe, { - ignore: ignoreList, - backend, - }) - const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => { - log.error("failed to subscribe to vcsDir", { error: err }) - pending.then((s) => s.unsubscribe()).catch(() => {}) - return undefined - }) - if (sub) { - subs.push(sub) - watchedPaths.add(vcsDir) - log.info("watching", { path: vcsDir }) + const isGit = Instance.project.vcs === "git" + if (isGit) { + const vcsDir = await $`git rev-parse --git-dir` + .quiet() + .nothrow() + .cwd(Instance.worktree) + .text() + .then((x) => path.resolve(Instance.worktree, x.trim())) + if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) { + const gitDirContents = await readdir(vcsDir).catch(() => []) + const ignoreList = gitDirContents.filter((entry) => entry !== "HEAD") + const pending = watcher().subscribe(vcsDir, subscribe, { + ignore: ignoreList, + backend, + }) + const sub = await withTimeout(pending, SUBSCRIBE_TIMEOUT_MS).catch((err) => { + log.error("failed to subscribe", { path: vcsDir, error: err }) + pending.then((s) => s.unsubscribe()).catch(() => {}) + return undefined + }) + if (sub) { + subs.push(sub) + watchedPaths.add(vcsDir) + log.info("watching", { path: vcsDir }) + } } } diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 8fa01522127..3eb15fb8516 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -16,7 +16,13 @@ export namespace Skill { const log = Log.create({ service: "skill" }) export const Events = { - Updated: BusEvent.define("skill.updated", z.object({})), + Updated: BusEvent.define( + "skill.updated", + z.record( + z.string(), + z.lazy(() => Info), + ), + ), } export const Info = z.object({ @@ -139,12 +145,35 @@ export namespace Skill { if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return Bus.subscribe(FileWatcher.Event.Updated, async (event) => { - const filename = path.basename(event.properties.file) - if (filename !== "SKILL.md") return + const filepath = event.properties.file.replaceAll("\\", "/") + const isSkillFile = filepath.endsWith("SKILL.md") + const isUnlink = event.properties.event === "unlink" + + const configRoot = Global.Path.config.replaceAll("\\", "/") + const configDirs = await Config.directories() + const normalizedDirs = configDirs.map((dir) => dir.replaceAll("\\", "/")) + const looksLikeConfigDir = + filepath.includes("/.opencode/") || + filepath.startsWith(".opencode/") || + filepath.includes("/.config/opencode/") || + filepath.startsWith(".config/opencode/") + const looksLikeClaudeDir = filepath.includes("/.claude/") || filepath.startsWith(".claude/") + const inConfigDir = + looksLikeConfigDir || + filepath === configRoot || + filepath.startsWith(configRoot + "/") || + normalizedDirs.some((dir) => Filesystem.contains(dir, filepath) || filepath === dir) + + const segments = filepath.split("/").filter(Boolean) + const hasSkillSegment = segments.includes("skill") || segments.includes("skills") + const inSkillArea = hasSkillSegment && (inConfigDir || looksLikeClaudeDir) + const isSkillDir = isUnlink && inSkillArea + + if (!isSkillFile && !isSkillDir) return await Instance.invalidate(initState) - await state() - Bus.publish(Events.Updated, {}) + const skills = await state() + Bus.publish(Events.Updated, skills) }) } } From 36bbc84d009a386030b6d5a76a7a72e505c08e1e Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 18:31:19 +0000 Subject: [PATCH 09/27] fix: refresh agents on config change --- packages/opencode/src/agent/agent.ts | 18 +++++++++++++++--- packages/opencode/src/project/bootstrap.ts | 2 ++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 64875091916..23383e30a70 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -5,6 +5,8 @@ import { generateObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" import { Truncate } from "../tool/truncation" +import { Bus } from "@/bus" +import { Flag } from "@/flag/flag" import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" @@ -43,7 +45,7 @@ export namespace Agent { }) export type Info = z.infer - const state = Instance.state(async () => { + async function initState() { const cfg = await Config.get() const defaults = PermissionNext.fromConfig({ @@ -239,7 +241,9 @@ export namespace Agent { } return result - }) + } + + const state = Instance.state(initState) export async function get(agent: string) { return state().then((x) => x[agent]) @@ -250,7 +254,7 @@ export namespace Agent { return pipe( await state(), values(), - sortBy([(x) => (cfg.default_agent ? x.name === cfg.default_agent : x.name === "build"), "desc"]), + sortBy([(item) => (cfg.default_agent ? item.name === cfg.default_agent : item.name === "build"), "desc"]), ) } @@ -258,6 +262,14 @@ export namespace Agent { return state().then((x) => Object.keys(x)[0]) } + export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + + Bus.subscribe(Config.Events.Updated, async () => { + await Instance.invalidate(initState) + }) + } + export async function generate(input: { description: string; model?: { providerID: string; modelID: string } }) { const cfg = await Config.get() const defaultModel = input.model ?? (await Provider.defaultModel()) diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index ae6ed69b79e..6c009c754e9 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -14,6 +14,7 @@ import { ShareNext } from "@/share/share-next" import { Config } from "../config/config" import { Skill } from "../skill/skill" import { ToolRegistry } from "@/tool/registry" +import { Agent } from "@/agent/agent" import { Flag } from "@/flag/flag" export async function InstanceBootstrap() { @@ -26,6 +27,7 @@ export async function InstanceBootstrap() { FileWatcher.init() if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { Skill.initWatcher() + Agent.initWatcher() Command.initWatcher() } if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) { From 9daf571f600e7e320f65e31d219644d4d691faf9 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 18:51:43 +0000 Subject: [PATCH 10/27] fix: broaden config hot-reload triggers --- packages/opencode/src/config/config.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ea6063365ac..bf0443735dc 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1239,8 +1239,6 @@ export namespace Config { Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") const isConfigFile = filepath.endsWith("opencode.json") || filepath.endsWith("opencode.jsonc") - const isConfigExtension = filepath.endsWith(".json") || filepath.endsWith(".jsonc") || filepath.endsWith(".md") - const isUnlink = event.properties.event === "unlink" const configRoot = Global.Path.config.replaceAll("\\", "/") const configDirs = await directories() @@ -1256,7 +1254,7 @@ export namespace Config { filepath.startsWith(configRoot + "/") || normalizedDirs.some((dir) => Filesystem.contains(dir, filepath) || filepath === dir) - const shouldInvalidate = isConfigFile || (inConfigDir && (isConfigExtension || isUnlink)) + const shouldInvalidate = isConfigFile || inConfigDir if (!shouldInvalidate) return await Instance.invalidate(initState) From cf290d732dd105af29ec6a0f0789eca891f4412a Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 18:54:25 +0000 Subject: [PATCH 11/27] fix: always watch global config dir --- packages/opencode/src/file/watcher.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 4fab7e293f2..15509a494ef 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -5,6 +5,7 @@ import { Instance } from "../project/instance" import { Log } from "../util/log" import { FileIgnore } from "./ignore" import { Config } from "../config/config" +import { Global } from "@/global" import path from "path" // @ts-ignore import { createWrapper } from "@parcel/watcher/wrapper" @@ -145,6 +146,7 @@ export namespace FileWatcher { if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC() || Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) { const configDirectories = await Config.directories() + if (!configDirectories.includes(Global.Path.config)) configDirectories.push(Global.Path.config) for (const dir of configDirectories) { if (isInsideWatchedPath(dir)) { log.debug("skipping duplicate watch", { path: dir, reason: "already inside watched path" }) From 396092e7312e46be5c4672ca34f5ccc6def5a478 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:02:29 +0000 Subject: [PATCH 12/27] fix(opencode): remove config hot reload --- packages/opencode/src/config/config.ts | 31 ---------------------- packages/opencode/src/project/bootstrap.ts | 6 ----- 2 files changed, 37 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index bf0443735dc..08273a2658b 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -21,7 +21,6 @@ import { ConfigMarkdown } from "./markdown" import { existsSync } from "fs" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" -import { FileWatcher } from "@/file/watcher" export namespace Config { const log = Log.create({ service: "config" }) @@ -1232,34 +1231,4 @@ export namespace Config { export async function directories() { return state().then((x) => x.directories) } - - export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) return - - Bus.subscribe(FileWatcher.Event.Updated, async (event) => { - const filepath = event.properties.file.replaceAll("\\", "/") - const isConfigFile = filepath.endsWith("opencode.json") || filepath.endsWith("opencode.jsonc") - - const configRoot = Global.Path.config.replaceAll("\\", "/") - const configDirs = await directories() - const normalizedDirs = configDirs.map((dir) => dir.replaceAll("\\", "/")) - const looksLikeConfigDir = - filepath.includes("/.opencode/") || - filepath.startsWith(".opencode/") || - filepath.includes("/.config/opencode/") || - filepath.startsWith(".config/opencode/") - const inConfigDir = - looksLikeConfigDir || - filepath === configRoot || - filepath.startsWith(configRoot + "/") || - normalizedDirs.some((dir) => Filesystem.contains(dir, filepath) || filepath === dir) - - const shouldInvalidate = isConfigFile || inConfigDir - if (!shouldInvalidate) return - - await Instance.invalidate(initState) - const cfg = await get() - Bus.publish(Events.Updated, cfg) - }) - } } diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 6c009c754e9..920a633be59 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -11,9 +11,7 @@ import { Instance } from "./instance" import { Vcs } from "./vcs" import { Log } from "@/util/log" import { ShareNext } from "@/share/share-next" -import { Config } from "../config/config" import { Skill } from "../skill/skill" -import { ToolRegistry } from "@/tool/registry" import { Agent } from "@/agent/agent" import { Flag } from "@/flag/flag" @@ -30,10 +28,6 @@ export async function InstanceBootstrap() { Agent.initWatcher() Command.initWatcher() } - if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) { - Config.initWatcher() - ToolRegistry.initWatcher() - } File.init() Vcs.init() From 6014eaadecd9e3af12850680e8e02a92f54c21a4 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:05:44 +0000 Subject: [PATCH 13/27] fix(opencode): limit hot reload to agent files --- packages/opencode/src/file/watcher.ts | 3 +-- packages/opencode/src/tool/registry.ts | 14 +------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 15509a494ef..99ed114f7f2 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -144,9 +144,8 @@ export namespace FileWatcher { } } - if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC() || Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) { + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { const configDirectories = await Config.directories() - if (!configDirectories.includes(Global.Path.config)) configDirectories.push(Global.Path.config) for (const dir of configDirectories) { if (isInsideWatchedPath(dir)) { log.debug("skipping duplicate watch", { path: dir, reason: "already inside watched path" }) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 5055a84eff8..ccbb23a864d 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -12,7 +12,6 @@ import { WriteTool } from "./write" import { InvalidTool } from "./invalid" import { SkillTool } from "./skill" import { Skill } from "../skill/skill" -import { Bus } from "@/bus" import type { Agent } from "../agent/agent" import { Tool } from "./tool" import { Instance } from "../project/instance" @@ -36,10 +35,7 @@ export namespace ToolRegistry { const custom = [] as Tool.Info[] const glob = new Bun.Glob("{tool,tools}/*.{js,ts}") - const cacheBust = - Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG() || Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC() - ? `?t=${Date.now()}` - : "" + const cacheBust = Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC() ? `?t=${Date.now()}` : "" for (const dir of await Config.directories()) { for await (const match of glob.scan({ @@ -149,12 +145,4 @@ export namespace ToolRegistry { ) return result } - - export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG()) return - - Bus.subscribe(Config.Events.Updated, async () => { - await Instance.invalidate(initState) - }) - } } From abe47cc13c11a156a8b440b3a67055dda9e35649 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:06:41 +0000 Subject: [PATCH 14/27] fix(opencode): drop config hot reload flag --- packages/opencode/src/flag/flag.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 7732a2a66be..99b7fe459f2 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -41,7 +41,6 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") export const OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC = () => truthy("OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC") - export const OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG = () => truthy("OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG") function truthy(key: string) { const value = process.env[key]?.toLowerCase() From 84e47d1708d534fb1d66ddd32e10a886bfc09587 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:07:46 +0000 Subject: [PATCH 15/27] chore(sdk): regenerate api --- packages/sdk/js/src/v2/gen/types.gen.ts | 40 ++++++++++++++----------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index cbace013033..d198027633b 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -40,21 +40,6 @@ export type EventProjectUpdated = { properties: Project } -export type EventServerInstanceDisposed = { - type: "server.instance.disposed" - properties: { - directory: string - } -} - -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - /** * Custom keybind configurations */ @@ -933,6 +918,13 @@ export type EventConfigUpdated = { properties: Config } +export type EventServerInstanceDisposed = { + type: "server.instance.disposed" + properties: { + directory: string + } +} + export type EventLspClientDiagnostics = { type: "lsp.client.diagnostics" properties: { @@ -1515,10 +1507,22 @@ export type EventTodoUpdated = { } } +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + export type EventSkillUpdated = { type: "skill.updated" properties: { - [key: string]: unknown + [key: string]: { + name: string + description: string + location: string + } } } @@ -1741,9 +1745,8 @@ export type Event = | EventInstallationUpdated | EventInstallationUpdateAvailable | EventProjectUpdated - | EventServerInstanceDisposed - | EventFileWatcherUpdated | EventConfigUpdated + | EventServerInstanceDisposed | EventLspClientDiagnostics | EventLspUpdated | EventMessageUpdated @@ -1760,6 +1763,7 @@ export type Event = | EventSessionCompacted | EventFileEdited | EventTodoUpdated + | EventFileWatcherUpdated | EventSkillUpdated | EventTuiPromptAppend | EventTuiCommandExecute From 0fb6b50fc5a8f39ec6ae86d0c42acf44adb58482 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:13:45 +0000 Subject: [PATCH 16/27] fix(opencode): restore agentic hot reload --- packages/opencode/src/agent/agent.ts | 29 ++++++++++++++++++++++++- packages/opencode/src/command/index.ts | 30 +++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 23383e30a70..2fa84b6f44d 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -7,6 +7,8 @@ import { Instance } from "../project/instance" import { Truncate } from "../tool/truncation" import { Bus } from "@/bus" import { Flag } from "@/flag/flag" +import { FileWatcher } from "@/file/watcher" +import { Filesystem } from "@/util/filesystem" import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" @@ -265,7 +267,32 @@ export namespace Agent { export function initWatcher() { if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return - Bus.subscribe(Config.Events.Updated, async () => { + Bus.subscribe(FileWatcher.Event.Updated, async (event) => { + const filepath = event.properties.file.replaceAll("\\", "/") + const isUnlink = event.properties.event === "unlink" + + const configRoot = Global.Path.config.replaceAll("\\", "/") + const configDirs = await Config.directories() + const normalizedDirs = configDirs.map((dir) => dir.replaceAll("\\", "/")) + const looksLikeConfigDir = + filepath.includes("/.opencode/") || + filepath.startsWith(".opencode/") || + filepath.includes("/.config/opencode/") || + filepath.startsWith(".config/opencode/") + const inConfigDir = + looksLikeConfigDir || + filepath === configRoot || + filepath.startsWith(configRoot + "/") || + normalizedDirs.some((dir) => Filesystem.contains(dir, filepath) || filepath === dir) + + const segments = filepath.split("/").filter(Boolean) + const hasAgentSegment = segments.includes("agent") || segments.includes("agents") + const inAgentArea = inConfigDir && hasAgentSegment + const isAgentFile = inAgentArea && filepath.endsWith(".md") + const isAgentDir = isUnlink && inAgentArea + + if (!isAgentFile && !isAgentDir) return + await Instance.invalidate(initState) }) } diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index d94fe91b99c..cdfdfa503ee 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -2,6 +2,9 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" import z from "zod" import { Config } from "../config/config" +import { FileWatcher } from "@/file/watcher" +import { Filesystem } from "@/util/filesystem" +import { Global } from "@/global" import { Instance } from "../project/instance" import { Identifier } from "../id/id" import PROMPT_INITIALIZE from "./template/initialize.txt" @@ -137,7 +140,32 @@ export namespace Command { export function initWatcher() { if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return - Bus.subscribe(Config.Events.Updated, async () => { + Bus.subscribe(FileWatcher.Event.Updated, async (event) => { + const filepath = event.properties.file.replaceAll("\\", "/") + const isUnlink = event.properties.event === "unlink" + + const configRoot = Global.Path.config.replaceAll("\\", "/") + const configDirs = await Config.directories() + const normalizedDirs = configDirs.map((dir) => dir.replaceAll("\\", "/")) + const looksLikeConfigDir = + filepath.includes("/.opencode/") || + filepath.startsWith(".opencode/") || + filepath.includes("/.config/opencode/") || + filepath.startsWith(".config/opencode/") + const inConfigDir = + looksLikeConfigDir || + filepath === configRoot || + filepath.startsWith(configRoot + "/") || + normalizedDirs.some((dir) => Filesystem.contains(dir, filepath) || filepath === dir) + + const segments = filepath.split("/").filter(Boolean) + const hasCommandSegment = segments.includes("command") || segments.includes("commands") + const inCommandArea = inConfigDir && hasCommandSegment + const isCommandFile = inCommandArea && filepath.endsWith(".md") + const isCommandDir = isUnlink && inCommandArea + + if (!isCommandFile && !isCommandDir) return + await Instance.invalidate(initState) await list() Bus.publish(Event.Updated, {}) From 9b52e2654e3b93e6d69b1dfd6611c505db80fa99 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:16:03 +0000 Subject: [PATCH 17/27] fix(app): refresh agents on file watch --- packages/app/src/context/global-sync.tsx | 21 +++++++++++++++++++ .../opencode/src/cli/cmd/tui/context/sync.tsx | 20 ++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index ef6bb21733b..05f7658e4a2 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -304,6 +304,27 @@ function createGlobalSync() { bootstrapInstance(directory) break } + case "file.watcher.updated": { + const filepath = event.properties.file.replaceAll("\\", "/") + const segments = filepath.split("/").filter(Boolean) + const hasAgent = segments.includes("agent") || segments.includes("agents") + const hasCommand = segments.includes("command") || segments.includes("commands") + const hasSkill = segments.includes("skill") || segments.includes("skills") + const inConfig = filepath.includes("/.opencode/") || filepath.startsWith(".opencode/") + if (!inConfig) break + if (!hasAgent && !hasCommand && !hasSkill) break + + const sdk = createClient(directory) + const refresh = [] as Promise[] + if (hasAgent || hasSkill) { + refresh.push(sdk.app.agents().then((x) => setStore("agent", x.data ?? []))) + } + if (hasCommand) { + refresh.push(sdk.command.list().then((x) => setStore("command", x.data ?? []))) + } + Promise.all(refresh) + break + } case "session.updated": { const result = Binary.search(store.session, event.properties.info.id, (s) => s.id) if (event.properties.info.time.archived) { diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index d4978a76319..a931a851a5a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -111,6 +111,26 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ case "server.instance.disposed": bootstrap() break + case "file.watcher.updated": { + const filepath = event.properties.file.replaceAll("\\", "/") + const segments = filepath.split("/").filter(Boolean) + const hasAgent = segments.includes("agent") || segments.includes("agents") + const hasCommand = segments.includes("command") || segments.includes("commands") + const hasSkill = segments.includes("skill") || segments.includes("skills") + const inConfig = filepath.includes("/.opencode/") || filepath.startsWith(".opencode/") + if (!inConfig) break + if (!hasAgent && !hasCommand && !hasSkill) break + + const refresh = [] as Promise[] + if (hasAgent || hasSkill) { + refresh.push(sdk.client.app.agents().then((x) => setStore("agent", reconcile(x.data ?? [])))) + } + if (hasCommand) { + refresh.push(sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? [])))) + } + Promise.all(refresh) + break + } case "permission.replied": { const requests = store.permission[event.properties.sessionID] if (!requests) break From b006df3064330ce2340c52a8d0cca283a43c5648 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:18:31 +0000 Subject: [PATCH 18/27] fix(opencode): always enable agentic hot reload --- packages/opencode/src/agent/agent.ts | 2 -- packages/opencode/src/command/index.ts | 2 -- packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/project/bootstrap.ts | 8 +++----- packages/opencode/src/skill/skill.ts | 2 -- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 2fa84b6f44d..9a301e18a35 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -265,8 +265,6 @@ export namespace Agent { } export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return - Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") const isUnlink = event.properties.event === "unlink" diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index cdfdfa503ee..777b4a45161 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -138,8 +138,6 @@ export namespace Command { } export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return - Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") const isUnlink = event.properties.event === "unlink" diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 99ed114f7f2..c681c0d76df 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -144,7 +144,7 @@ export namespace FileWatcher { } } - if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { + { const configDirectories = await Config.directories() for (const dir of configDirectories) { if (isInsideWatchedPath(dir)) { diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 920a633be59..b3c49f527ce 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -23,11 +23,9 @@ export async function InstanceBootstrap() { Format.init() await LSP.init() FileWatcher.init() - if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { - Skill.initWatcher() - Agent.initWatcher() - Command.initWatcher() - } + Skill.initWatcher() + Agent.initWatcher() + Command.initWatcher() File.init() Vcs.init() diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 3eb15fb8516..fff805077be 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -142,8 +142,6 @@ export namespace Skill { } export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return - Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") const isSkillFile = filepath.endsWith("SKILL.md") From 3005b5aaf6c065780ba858017215350a7a426233 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:20:25 +0000 Subject: [PATCH 19/27] Revert "fix(opencode): always enable agentic hot reload" This reverts commit b006df3064330ce2340c52a8d0cca283a43c5648. --- packages/opencode/src/agent/agent.ts | 2 ++ packages/opencode/src/command/index.ts | 2 ++ packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/project/bootstrap.ts | 8 +++++--- packages/opencode/src/skill/skill.ts | 2 ++ 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 9a301e18a35..2fa84b6f44d 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -265,6 +265,8 @@ export namespace Agent { } export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") const isUnlink = event.properties.event === "unlink" diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 777b4a45161..cdfdfa503ee 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -138,6 +138,8 @@ export namespace Command { } export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") const isUnlink = event.properties.event === "unlink" diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index c681c0d76df..99ed114f7f2 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -144,7 +144,7 @@ export namespace FileWatcher { } } - { + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { const configDirectories = await Config.directories() for (const dir of configDirectories) { if (isInsideWatchedPath(dir)) { diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index b3c49f527ce..920a633be59 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -23,9 +23,11 @@ export async function InstanceBootstrap() { Format.init() await LSP.init() FileWatcher.init() - Skill.initWatcher() - Agent.initWatcher() - Command.initWatcher() + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { + Skill.initWatcher() + Agent.initWatcher() + Command.initWatcher() + } File.init() Vcs.init() diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index fff805077be..3eb15fb8516 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -142,6 +142,8 @@ export namespace Skill { } export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") const isSkillFile = filepath.endsWith("SKILL.md") From 60fd5d791e15a8870d4357d52a637be1e91cb86d Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:21:18 +0000 Subject: [PATCH 20/27] fix(opencode): reload agentic config on file changes --- packages/opencode/src/config/config.ts | 38 ++++++++++++++++++++++ packages/opencode/src/project/bootstrap.ts | 2 ++ 2 files changed, 40 insertions(+) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 08273a2658b..3b3505c01fc 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -21,6 +21,7 @@ import { ConfigMarkdown } from "./markdown" import { existsSync } from "fs" import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" +import { FileWatcher } from "@/file/watcher" export namespace Config { const log = Log.create({ service: "config" }) @@ -1231,4 +1232,41 @@ export namespace Config { export async function directories() { return state().then((x) => x.directories) } + + export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + + Bus.subscribe(FileWatcher.Event.Updated, async (event) => { + const filepath = event.properties.file.replaceAll("\\", "/") + const isUnlink = event.properties.event === "unlink" + + const configRoot = Global.Path.config.replaceAll("\\", "/") + const configDirs = await directories() + const normalizedDirs = configDirs.map((dir) => dir.replaceAll("\\", "/")) + const looksLikeConfigDir = + filepath.includes("/.opencode/") || + filepath.startsWith(".opencode/") || + filepath.includes("/.config/opencode/") || + filepath.startsWith(".config/opencode/") + const inConfigDir = + looksLikeConfigDir || + filepath === configRoot || + filepath.startsWith(configRoot + "/") || + normalizedDirs.some((dir) => Filesystem.contains(dir, filepath) || filepath === dir) + + const segments = filepath.split("/").filter(Boolean) + const hasAgent = segments.includes("agent") || segments.includes("agents") + const hasCommand = segments.includes("command") || segments.includes("commands") + const hasSkill = segments.includes("skill") || segments.includes("skills") + const inArea = inConfigDir && (hasAgent || hasCommand || hasSkill) + const isFile = inArea && filepath.endsWith(".md") + const isDir = isUnlink && inArea + + if (!isFile && !isDir) return + + await Instance.invalidate(initState) + const cfg = await get() + Bus.publish(Events.Updated, cfg) + }) + } } diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 920a633be59..3f8ec235dc8 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -11,6 +11,7 @@ import { Instance } from "./instance" import { Vcs } from "./vcs" import { Log } from "@/util/log" import { ShareNext } from "@/share/share-next" +import { Config } from "../config/config" import { Skill } from "../skill/skill" import { Agent } from "@/agent/agent" import { Flag } from "@/flag/flag" @@ -27,6 +28,7 @@ export async function InstanceBootstrap() { Skill.initWatcher() Agent.initWatcher() Command.initWatcher() + Config.initWatcher() } File.init() Vcs.init() From 9bbbe0f303dce34f3fb706e319a8f6c2e775ef33 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:25:04 +0000 Subject: [PATCH 21/27] fix(opencode): restore tool hot reload hook --- packages/opencode/src/tool/registry.ts | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index ccbb23a864d..f1633d2800a 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -16,6 +16,10 @@ import type { Agent } from "../agent/agent" import { Tool } from "./tool" import { Instance } from "../project/instance" import { Config } from "../config/config" +import { Bus } from "@/bus" +import { FileWatcher } from "@/file/watcher" +import { Filesystem } from "@/util/filesystem" +import { Global } from "@/global" import path from "path" import { type ToolDefinition } from "@opencode-ai/plugin" import z from "zod" @@ -120,6 +124,39 @@ export namespace ToolRegistry { ] } + export function initWatcher() { + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + + Bus.subscribe(FileWatcher.Event.Updated, async (event) => { + const filepath = event.properties.file.replaceAll("\\", "/") + const isUnlink = event.properties.event === "unlink" + + const configRoot = Global.Path.config.replaceAll("\\", "/") + const configDirs = await Config.directories() + const normalizedDirs = configDirs.map((dir) => dir.replaceAll("\\", "/")) + const looksLikeConfigDir = + filepath.includes("/.opencode/") || + filepath.startsWith(".opencode/") || + filepath.includes("/.config/opencode/") || + filepath.startsWith(".config/opencode/") + const inConfigDir = + looksLikeConfigDir || + filepath === configRoot || + filepath.startsWith(configRoot + "/") || + normalizedDirs.some((dir) => Filesystem.contains(dir, filepath) || filepath === dir) + + const segments = filepath.split("/").filter(Boolean) + const hasToolSegment = segments.includes("tool") || segments.includes("tools") + const inToolArea = inConfigDir && hasToolSegment + const isToolFile = inToolArea && (filepath.endsWith(".ts") || filepath.endsWith(".js")) + const isToolDir = isUnlink && inToolArea + + if (!isToolFile && !isToolDir) return + + await Instance.invalidate(initState) + }) + } + export async function ids() { return all().then((x) => x.map((t) => t.id)) } From 2a94351f1e64a4c7a711e236c02aff1dc27879ad Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:28:36 +0000 Subject: [PATCH 22/27] test(opencode): remove hot reload tests --- .../config/config-delete-hot-reload.test.ts | 114 ----- .../test/skill/skill-hot-reload.test.ts | 457 ------------------ 2 files changed, 571 deletions(-) delete mode 100644 packages/opencode/test/config/config-delete-hot-reload.test.ts delete mode 100644 packages/opencode/test/skill/skill-hot-reload.test.ts diff --git a/packages/opencode/test/config/config-delete-hot-reload.test.ts b/packages/opencode/test/config/config-delete-hot-reload.test.ts deleted file mode 100644 index 16e975e111b..00000000000 --- a/packages/opencode/test/config/config-delete-hot-reload.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { test, expect, beforeAll, afterAll, mock } from "bun:test" -import path from "path" -import fs from "fs/promises" - -// Mock FileIgnore to prevent ignoring /tmp directories during tests -mock.module("../../src/file/ignore.ts", () => { - return { - FileIgnore: { - PATTERNS: [".git"], // Keep .git ignored but allow everything else - match: () => false, - }, - } -}) - -import { tmpdir } from "../fixture/fixture.js" -import { Instance } from "../../src/project/instance.js" -import { FileWatcher } from "../../src/file/watcher.js" -import { Config } from "../../src/config/config.js" -import { Agent } from "../../src/agent/agent.js" -import { Command } from "../../src/command/index.js" -import { Bus } from "../../src/bus/index.js" -import { withTimeout } from "../../src/util/timeout.js" - -async function waitForWatcherReady(root: string, label: string) { - const readyPath = path.join(root, ".opencode", `watcher-ready-${label}.txt`) - const readyEvent = new Promise((resolve) => { - const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, (evt) => { - if (evt.properties.file.replaceAll("\\", "/") !== readyPath.replaceAll("\\", "/")) return - unsubscribe() - resolve() - }) - }) - await Bun.write(readyPath, "ready") - await withTimeout(readyEvent, 5000) -} - -test.skip("unlinking agent/command directories reloads config", async () => { - // Enable hot reload flags for this test - process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC"] = "true" - process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG"] = "true" - process.env["OPENCODE_EXPERIMENTAL_FILEWATCHER"] = "true" - - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - const opencodeDir = path.join(dir, ".opencode") - const agentDir = path.join(opencodeDir, "agent") - const commandDir = path.join(opencodeDir, "command") - await fs.mkdir(agentDir, { recursive: true }) - await fs.mkdir(commandDir, { recursive: true }) - - await Bun.write( - path.join(agentDir, "delete-test-agent.md"), - `--- -model: test/model -mode: subagent ---- -Delete test agent prompt`, - ) - - await Bun.write( - path.join(commandDir, "delete-test-command.md"), - `--- -description: delete test command ---- -Run $ARGUMENTS`, - ) - }, - }) - - await Instance.provide({ - directory: tmp.path, - init: async () => { - FileWatcher.init() - Config.initWatcher() - Command.initWatcher() - await new Promise((resolve) => setTimeout(resolve, 200)) - }, - fn: async () => { - const initialAgents = await Agent.list() - expect(initialAgents.find((agent) => agent.name === "delete-test-agent")).toBeDefined() - - const initialCommands = await Command.list() - expect(initialCommands.find((command) => command.name === "delete-test-command")).toBeDefined() - - await waitForWatcherReady(tmp.path, "config-delete") - - const waitForConfigUpdate = () => - new Promise((resolve) => { - const unsubscribe = Bus.subscribe(Config.Events.Updated, () => { - unsubscribe() - resolve() - }) - }) - - const agentDir = path.join(tmp.path, ".opencode", "agent") - const commandDir = path.join(tmp.path, ".opencode", "command") - await fs.rm(agentDir, { recursive: true, force: true }) - await fs.rm(commandDir, { recursive: true, force: true }) - - await withTimeout(waitForConfigUpdate(), 5000) - - const updatedAgents = await Agent.list() - expect(updatedAgents.find((agent) => agent.name === "delete-test-agent")).toBeUndefined() - - const updatedCommands = await Command.list() - expect(updatedCommands.find((command) => command.name === "delete-test-command")).toBeUndefined() - }, - }) - - // Cleanup env - delete process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC"] - delete process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG"] -}, 20000) diff --git a/packages/opencode/test/skill/skill-hot-reload.test.ts b/packages/opencode/test/skill/skill-hot-reload.test.ts deleted file mode 100644 index 441bfcc0658..00000000000 --- a/packages/opencode/test/skill/skill-hot-reload.test.ts +++ /dev/null @@ -1,457 +0,0 @@ -import { test, expect, describe, mock } from "bun:test" - -// Mock FileIgnore to prevent ignoring /tmp directories during tests -mock.module("../../src/file/ignore.ts", () => { - return { - FileIgnore: { - PATTERNS: [".git"], // Keep .git ignored but allow everything else - match: () => false, - }, - } -}) - -import { Skill } from "../../src/skill/skill.js" -import { SkillTool } from "../../src/tool/skill.js" -import { ToolRegistry } from "../../src/tool/registry.js" -import { Instance } from "../../src/project/instance.js" -import { tmpdir } from "../fixture/fixture.js" -import { FileWatcher } from "../../src/file/watcher.js" -import { Bus } from "../../src/bus/index.js" -import { withTimeout } from "../../src/util/timeout.js" -import path from "path" -import fs from "fs/promises" - -const TEST_TIMEOUT = 15000 -const EVENT_TIMEOUT = 5000 - -/** - * Wait for the file watcher to be ready by writing a marker file - * and waiting for its change event to be emitted. - */ -async function waitForWatcherReady(root: string, label: string) { - const readyPath = path.join(root, ".opencode", `watcher-ready-${label}.txt`) - const readyEvent = new Promise((resolve) => { - const unsubscribe = Bus.subscribe(FileWatcher.Event.Updated, (evt) => { - if (evt.properties.file.replaceAll("\\", "/") !== readyPath.replaceAll("\\", "/")) return - unsubscribe() - resolve() - }) - }) - await Bun.write(readyPath, `ready-${Date.now()}`) - await withTimeout(readyEvent, EVENT_TIMEOUT) -} - -/** - * Create a promise that resolves when Skill.Events.Updated is emitted. - */ -function waitForSkillUpdate(): Promise { - return new Promise((resolve) => { - const unsubscribe = Bus.subscribe(Skill.Events.Updated, () => { - unsubscribe() - resolve() - }) - }) -} - -/** - * Helper to create a skill file with proper frontmatter - */ -function createSkillContent(name: string, description: string, body = "Instructions."): string { - return `--- -name: ${name} -description: ${description} ---- - -# ${name} - -${body} -` -} - -describe("skill hot reload", () => { - // Enable hot reload flags for these tests - process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC"] = "true" - process.env["OPENCODE_EXPERIMENTAL_HOT_RELOAD_CONFIG"] = "true" - process.env["OPENCODE_EXPERIMENTAL_FILEWATCHER"] = "true" - - /** - * Test adding a new skill file triggers hot reload and updates both - * the skill list and tool description. - */ - test.skip( - "adding a new skill updates skill list and tool description", - async () => { - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - // Create initial skill so we have something to start with - const skillDir = path.join(dir, ".opencode", "skill", "existing-skill") - await fs.mkdir(skillDir, { recursive: true }) - await Bun.write(path.join(skillDir, "SKILL.md"), createSkillContent("existing-skill", "An existing skill.")) - }, - }) - - await Instance.provide({ - directory: tmp.path, - init: async () => { - FileWatcher.init() - Skill.initWatcher() - ToolRegistry.initWatcher() - await new Promise((resolve) => setTimeout(resolve, 200)) - }, - fn: async () => { - // Verify initial state - const initialSkills = await Skill.all() - expect(initialSkills.find((s) => s.name === "existing-skill")).toBeDefined() - expect(initialSkills.find((s) => s.name === "new-skill")).toBeUndefined() - - const initialToolConfig = await SkillTool.init({}) - expect(initialToolConfig.description).toContain("existing-skill") - expect(initialToolConfig.description).not.toContain("new-skill") - - // Ensure watcher is ready - await waitForWatcherReady(tmp.path, "add-skill") - - // Set up listener BEFORE making the change - const updatePromise = waitForSkillUpdate() - - // Add a new skill - const newSkillDir = path.join(tmp.path, ".opencode", "skill", "new-skill") - await fs.mkdir(newSkillDir, { recursive: true }) - await Bun.write(path.join(newSkillDir, "SKILL.md"), createSkillContent("new-skill", "A newly added skill.")) - - // Wait for the skill update event - await withTimeout(updatePromise, EVENT_TIMEOUT) - - // Verify new skill is available - const updatedSkills = await Skill.all() - const newSkill = updatedSkills.find((s) => s.name === "new-skill") - expect(newSkill).toBeDefined() - expect(newSkill?.description).toBe("A newly added skill.") - - // Verify tool description includes new skill - const updatedToolConfig = await SkillTool.init({}) - expect(updatedToolConfig.description).toContain("new-skill") - expect(updatedToolConfig.description).toContain("A newly added skill.") - }, - }) - }, - TEST_TIMEOUT, - ) - - /** - * Test modifying an existing skill file triggers hot reload. - */ - test.skip( - "modifying an existing skill updates skill list and tool description", - async () => { - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - // Create the skill that we will modify - const skillDir = path.join(dir, ".opencode", "skill", "modifiable-skill") - await fs.mkdir(skillDir, { recursive: true }) - await Bun.write( - path.join(skillDir, "SKILL.md"), - createSkillContent("modifiable-skill", "Original description.", "Original content."), - ) - }, - }) - - await Instance.provide({ - directory: tmp.path, - init: async () => { - FileWatcher.init() - Skill.initWatcher() - ToolRegistry.initWatcher() - await new Promise((resolve) => setTimeout(resolve, 200)) - }, - fn: async () => { - // Verify initial state - const initialSkills = await Skill.all() - const initialSkill = initialSkills.find((s) => s.name === "modifiable-skill") - expect(initialSkill).toBeDefined() - expect(initialSkill?.description).toBe("Original description.") - - const initialToolConfig = await SkillTool.init({}) - expect(initialToolConfig.description).toContain("Original description.") - - // Ensure watcher is ready - await waitForWatcherReady(tmp.path, "modify-skill") - - // Set up listener BEFORE making the change - const updatePromise = waitForSkillUpdate() - - // Modify the existing skill file - const skillPath = path.join(tmp.path, ".opencode", "skill", "modifiable-skill", "SKILL.md") - await Bun.write(skillPath, createSkillContent("modifiable-skill", "Updated description.", "Updated content.")) - - // Wait for the skill update event - await withTimeout(updatePromise, EVENT_TIMEOUT) - - // Verify skill description is updated - const updatedSkills = await Skill.all() - const updatedSkill = updatedSkills.find((s) => s.name === "modifiable-skill") - expect(updatedSkill).toBeDefined() - expect(updatedSkill?.description).toBe("Updated description.") - - // Verify tool description is updated - const updatedToolConfig = await SkillTool.init({}) - expect(updatedToolConfig.description).toContain("Updated description.") - expect(updatedToolConfig.description).not.toContain("Original description.") - }, - }) - }, - TEST_TIMEOUT, - ) - - /** - * Test deleting a skill file (SKILL.md) triggers hot reload. - */ - test.skip( - "deleting a skill file removes it from skill list and tool description", - async () => { - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - // Create two skills - one to keep, one to delete - const keepDir = path.join(dir, ".opencode", "skill", "keeper-skill") - const deleteDir = path.join(dir, ".opencode", "skill", "deletable-skill") - await fs.mkdir(keepDir, { recursive: true }) - await fs.mkdir(deleteDir, { recursive: true }) - await Bun.write(path.join(keepDir, "SKILL.md"), createSkillContent("keeper-skill", "This skill stays.")) - await Bun.write( - path.join(deleteDir, "SKILL.md"), - createSkillContent("deletable-skill", "This skill will be removed."), - ) - }, - }) - - await Instance.provide({ - directory: tmp.path, - init: async () => { - FileWatcher.init() - Skill.initWatcher() - ToolRegistry.initWatcher() - await new Promise((resolve) => setTimeout(resolve, 200)) - }, - fn: async () => { - // Verify initial state has both skills - const initialSkills = await Skill.all() - expect(initialSkills.find((s) => s.name === "keeper-skill")).toBeDefined() - expect(initialSkills.find((s) => s.name === "deletable-skill")).toBeDefined() - - const initialToolConfig = await SkillTool.init({}) - expect(initialToolConfig.description).toContain("keeper-skill") - expect(initialToolConfig.description).toContain("deletable-skill") - - // Ensure watcher is ready - await waitForWatcherReady(tmp.path, "delete-skill-file") - - // Set up listener BEFORE making the change - const updatePromise = waitForSkillUpdate() - - // Delete just the SKILL.md file (not the directory) - const skillFilePath = path.join(tmp.path, ".opencode", "skill", "deletable-skill", "SKILL.md") - await fs.unlink(skillFilePath) - - // Wait for the skill update event - await withTimeout(updatePromise, EVENT_TIMEOUT) - - // Verify deleted skill is removed - const updatedSkills = await Skill.all() - expect(updatedSkills.find((s) => s.name === "keeper-skill")).toBeDefined() - expect(updatedSkills.find((s) => s.name === "deletable-skill")).toBeUndefined() - - // Verify tool description no longer contains deleted skill - const updatedToolConfig = await SkillTool.init({}) - expect(updatedToolConfig.description).toContain("keeper-skill") - expect(updatedToolConfig.description).not.toContain("deletable-skill") - }, - }) - }, - TEST_TIMEOUT, - ) - - /** - * Test deleting an entire skill directory triggers hot reload. - */ - test.skip( - "deleting a skill directory removes it from skill list and tool description", - async () => { - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - // Create two skills - one to keep, one to delete - const keepDir = path.join(dir, ".opencode", "skill", "persistent-skill") - const deleteDir = path.join(dir, ".opencode", "skill", "removable-skill") - await fs.mkdir(keepDir, { recursive: true }) - await fs.mkdir(deleteDir, { recursive: true }) - await Bun.write( - path.join(keepDir, "SKILL.md"), - createSkillContent("persistent-skill", "This skill persists."), - ) - await Bun.write( - path.join(deleteDir, "SKILL.md"), - createSkillContent("removable-skill", "This skill directory will be deleted."), - ) - }, - }) - - await Instance.provide({ - directory: tmp.path, - init: async () => { - FileWatcher.init() - Skill.initWatcher() - ToolRegistry.initWatcher() - await new Promise((resolve) => setTimeout(resolve, 200)) - }, - fn: async () => { - // Verify initial state has both skills - const initialSkills = await Skill.all() - expect(initialSkills.find((s) => s.name === "persistent-skill")).toBeDefined() - expect(initialSkills.find((s) => s.name === "removable-skill")).toBeDefined() - - const initialToolConfig = await SkillTool.init({}) - expect(initialToolConfig.description).toContain("persistent-skill") - expect(initialToolConfig.description).toContain("removable-skill") - - // Ensure watcher is ready - await waitForWatcherReady(tmp.path, "delete-skill-dir") - - // Set up listener BEFORE making the change - const updatePromise = waitForSkillUpdate() - - // Delete the entire skill directory - const skillDirPath = path.join(tmp.path, ".opencode", "skill", "removable-skill") - await fs.rm(skillDirPath, { recursive: true, force: true }) - - // Wait for the skill update event - await withTimeout(updatePromise, EVENT_TIMEOUT) - - // Verify deleted skill is removed - const updatedSkills = await Skill.all() - expect(updatedSkills.find((s) => s.name === "persistent-skill")).toBeDefined() - expect(updatedSkills.find((s) => s.name === "removable-skill")).toBeUndefined() - - // Verify tool description no longer contains deleted skill - const updatedToolConfig = await SkillTool.init({}) - expect(updatedToolConfig.description).toContain("persistent-skill") - expect(updatedToolConfig.description).not.toContain("removable-skill") - }, - }) - }, - TEST_TIMEOUT, - ) - - /** - * Test renaming a skill (changing the name field in frontmatter) - */ - test.skip( - "renaming a skill name in frontmatter updates the skill list", - async () => { - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - const skillDir = path.join(dir, ".opencode", "skill", "renamable-skill") - await fs.mkdir(skillDir, { recursive: true }) - await Bun.write(path.join(skillDir, "SKILL.md"), createSkillContent("old-name", "A skill to be renamed.")) - }, - }) - - await Instance.provide({ - directory: tmp.path, - init: async () => { - FileWatcher.init() - Skill.initWatcher() - ToolRegistry.initWatcher() - await new Promise((resolve) => setTimeout(resolve, 200)) - }, - fn: async () => { - // Verify initial state - const initialSkills = await Skill.all() - expect(initialSkills.find((s) => s.name === "old-name")).toBeDefined() - expect(initialSkills.find((s) => s.name === "new-name")).toBeUndefined() - - // Ensure watcher is ready - await waitForWatcherReady(tmp.path, "rename-skill") - - // Set up listener BEFORE making the change - const updatePromise = waitForSkillUpdate() - - // Rename the skill by changing the frontmatter - const skillPath = path.join(tmp.path, ".opencode", "skill", "renamable-skill", "SKILL.md") - await Bun.write(skillPath, createSkillContent("new-name", "A renamed skill.")) - - // Wait for the skill update event - await withTimeout(updatePromise, EVENT_TIMEOUT) - - // Verify skill is renamed - const updatedSkills = await Skill.all() - expect(updatedSkills.find((s) => s.name === "old-name")).toBeUndefined() - expect(updatedSkills.find((s) => s.name === "new-name")).toBeDefined() - expect(updatedSkills.find((s) => s.name === "new-name")?.description).toBe("A renamed skill.") - }, - }) - }, - TEST_TIMEOUT, - ) - - /** - * Test that adding multiple skills in sequence works correctly - */ - test.skip( - "adding multiple skills in sequence updates correctly", - async () => { - await using tmp = await tmpdir({ - git: true, - init: async (dir) => { - // Create .opencode/skill directory - await fs.mkdir(path.join(dir, ".opencode", "skill"), { recursive: true }) - }, - }) - - await Instance.provide({ - directory: tmp.path, - init: async () => { - FileWatcher.init() - Skill.initWatcher() - ToolRegistry.initWatcher() - await new Promise((resolve) => setTimeout(resolve, 200)) - }, - fn: async () => { - // Verify no skills initially - const initialSkills = await Skill.all() - expect(initialSkills).toHaveLength(0) - - // Ensure watcher is ready - await waitForWatcherReady(tmp.path, "multi-add") - - // Add first skill - const updatePromise1 = waitForSkillUpdate() - const skill1Dir = path.join(tmp.path, ".opencode", "skill", "skill-one") - await fs.mkdir(skill1Dir, { recursive: true }) - await Bun.write(path.join(skill1Dir, "SKILL.md"), createSkillContent("skill-one", "First skill.")) - await withTimeout(updatePromise1, EVENT_TIMEOUT) - - const afterFirst = await Skill.all() - expect(afterFirst).toHaveLength(1) - expect(afterFirst.find((s) => s.name === "skill-one")).toBeDefined() - - // Add second skill - const updatePromise2 = waitForSkillUpdate() - const skill2Dir = path.join(tmp.path, ".opencode", "skill", "skill-two") - await fs.mkdir(skill2Dir, { recursive: true }) - await Bun.write(path.join(skill2Dir, "SKILL.md"), createSkillContent("skill-two", "Second skill.")) - await withTimeout(updatePromise2, EVENT_TIMEOUT) - - const afterSecond = await Skill.all() - expect(afterSecond).toHaveLength(2) - expect(afterSecond.find((s) => s.name === "skill-one")).toBeDefined() - expect(afterSecond.find((s) => s.name === "skill-two")).toBeDefined() - }, - }) - }, - TEST_TIMEOUT, - ) -}) From 00e407cac0674741e2bbd52f240a49489fdeca3e Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:38:21 +0000 Subject: [PATCH 23/27] refactor(opencode): rename hot reload flag --- packages/opencode/src/agent/agent.ts | 2 +- packages/opencode/src/command/index.ts | 2 +- packages/opencode/src/config/config.ts | 2 +- packages/opencode/src/file/watcher.ts | 2 +- packages/opencode/src/flag/flag.ts | 2 +- packages/opencode/src/project/bootstrap.ts | 2 +- packages/opencode/src/skill/skill.ts | 2 +- packages/opencode/src/tool/registry.ts | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 2fa84b6f44d..469316eb373 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -265,7 +265,7 @@ export namespace Agent { } export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD()) return Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index cdfdfa503ee..01fc6f3c7eb 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -138,7 +138,7 @@ export namespace Command { } export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD()) return Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3b3505c01fc..4452c943259 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1234,7 +1234,7 @@ export namespace Config { } export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD()) return Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index 99ed114f7f2..67f60c06684 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -144,7 +144,7 @@ export namespace FileWatcher { } } - if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD()) { const configDirectories = await Config.directories() for (const dir of configDirectories) { if (isInsideWatchedPath(dir)) { diff --git a/packages/opencode/src/flag/flag.ts b/packages/opencode/src/flag/flag.ts index 99b7fe459f2..38566bb0797 100644 --- a/packages/opencode/src/flag/flag.ts +++ b/packages/opencode/src/flag/flag.ts @@ -40,7 +40,7 @@ export namespace Flag { export const OPENCODE_EXPERIMENTAL_LSP_TOOL = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_LSP_TOOL") export const OPENCODE_EXPERIMENTAL_PLAN_MODE = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_PLAN_MODE") - export const OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC = () => truthy("OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC") + export const OPENCODE_EXPERIMENTAL_HOT_RELOAD = () => truthy("OPENCODE_EXPERIMENTAL_HOT_RELOAD") function truthy(key: string) { const value = process.env[key]?.toLowerCase() diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 3f8ec235dc8..faef2a769b8 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -24,7 +24,7 @@ export async function InstanceBootstrap() { Format.init() await LSP.init() FileWatcher.init() - if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) { + if (Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD()) { Skill.initWatcher() Agent.initWatcher() Command.initWatcher() diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 3eb15fb8516..931870da612 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -142,7 +142,7 @@ export namespace Skill { } export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD()) return Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index f1633d2800a..1556848f5ee 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -39,7 +39,7 @@ export namespace ToolRegistry { const custom = [] as Tool.Info[] const glob = new Bun.Glob("{tool,tools}/*.{js,ts}") - const cacheBust = Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC() ? `?t=${Date.now()}` : "" + const cacheBust = Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD() ? `?t=${Date.now()}` : "" for (const dir of await Config.directories()) { for await (const match of glob.scan({ @@ -125,7 +125,7 @@ export namespace ToolRegistry { } export function initWatcher() { - if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD_AGENTIC()) return + if (!Flag.OPENCODE_EXPERIMENTAL_HOT_RELOAD()) return Bus.subscribe(FileWatcher.Event.Updated, async (event) => { const filepath = event.properties.file.replaceAll("\\", "/") From e4cb7cc2356ae93fdcc3bcb4f4f447c7d1466070 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:40:41 +0000 Subject: [PATCH 24/27] chore(sdk): align generated types with dev --- packages/sdk/js/src/v2/gen/types.gen.ts | 3062 +++++++++++------------ 1 file changed, 1422 insertions(+), 1640 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index d198027633b..97a695162ed 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -40,1768 +40,1649 @@ export type EventProjectUpdated = { properties: Project } -/** - * Custom keybind configurations - */ -export type KeybindsConfig = { - /** - * Leader key for keybind combinations - */ - leader?: string - /** - * Exit the application - */ - app_exit?: string - /** - * Open external editor - */ - editor_open?: string - /** - * List available themes - */ - theme_list?: string - /** - * Toggle sidebar - */ - sidebar_toggle?: string - /** - * Toggle session scrollbar - */ - scrollbar_toggle?: string - /** - * Toggle username visibility - */ - username_toggle?: string - /** - * View status - */ - status_view?: string - /** - * Export session to editor - */ - session_export?: string - /** - * Create a new session - */ - session_new?: string - /** - * List all sessions - */ - session_list?: string - /** - * Show session timeline - */ - session_timeline?: string - /** - * Fork session from message - */ - session_fork?: string - /** - * Rename session - */ - session_rename?: string - /** - * Delete session - */ - session_delete?: string - /** - * Delete stash entry - */ - stash_delete?: string - /** - * Open provider list from model dialog - */ - model_provider_list?: string - /** - * Toggle model favorite status - */ - model_favorite_toggle?: string - /** - * Share current session - */ - session_share?: string - /** - * Unshare current session - */ - session_unshare?: string - /** - * Interrupt current session - */ - session_interrupt?: string - /** - * Compact the session - */ - session_compact?: string - /** - * Scroll messages up by one page - */ - messages_page_up?: string - /** - * Scroll messages down by one page - */ - messages_page_down?: string - /** - * Scroll messages up by half page - */ - messages_half_page_up?: string - /** - * Scroll messages down by half page - */ - messages_half_page_down?: string - /** - * Navigate to first message - */ - messages_first?: string - /** - * Navigate to last message - */ - messages_last?: string - /** - * Navigate to next message - */ - messages_next?: string - /** - * Navigate to previous message - */ - messages_previous?: string - /** - * Navigate to last user message - */ - messages_last_user?: string - /** - * Copy message - */ - messages_copy?: string - /** - * Undo message - */ - messages_undo?: string - /** - * Redo message - */ - messages_redo?: string - /** - * Toggle code block concealment in messages - */ - messages_toggle_conceal?: string - /** - * Toggle tool details visibility - */ - tool_details?: string - /** - * List available models - */ - model_list?: string - /** - * Next recently used model - */ - model_cycle_recent?: string - /** - * Previous recently used model - */ - model_cycle_recent_reverse?: string - /** - * Next favorite model - */ - model_cycle_favorite?: string - /** - * Previous favorite model - */ - model_cycle_favorite_reverse?: string - /** - * List available commands - */ - command_list?: string - /** - * List agents - */ - agent_list?: string - /** - * Next agent - */ - agent_cycle?: string - /** - * Previous agent - */ - agent_cycle_reverse?: string - /** - * Cycle model variants - */ - variant_cycle?: string - /** - * Clear input field - */ - input_clear?: string - /** - * Paste from clipboard - */ - input_paste?: string - /** - * Submit input - */ - input_submit?: string - /** - * Insert newline in input - */ - input_newline?: string - /** - * Move cursor left in input - */ - input_move_left?: string - /** - * Move cursor right in input - */ - input_move_right?: string - /** - * Move cursor up in input - */ - input_move_up?: string - /** - * Move cursor down in input - */ - input_move_down?: string - /** - * Select left in input - */ - input_select_left?: string - /** - * Select right in input - */ - input_select_right?: string - /** - * Select up in input - */ - input_select_up?: string - /** - * Select down in input - */ - input_select_down?: string - /** - * Move to start of line in input - */ - input_line_home?: string - /** - * Move to end of line in input - */ - input_line_end?: string - /** - * Select to start of line in input - */ - input_select_line_home?: string - /** - * Select to end of line in input - */ - input_select_line_end?: string - /** - * Move to start of visual line in input - */ - input_visual_line_home?: string - /** - * Move to end of visual line in input - */ - input_visual_line_end?: string - /** - * Select to start of visual line in input - */ - input_select_visual_line_home?: string - /** - * Select to end of visual line in input - */ - input_select_visual_line_end?: string - /** - * Move to start of buffer in input - */ - input_buffer_home?: string - /** - * Move to end of buffer in input - */ - input_buffer_end?: string - /** - * Select to start of buffer in input - */ - input_select_buffer_home?: string - /** - * Select to end of buffer in input - */ - input_select_buffer_end?: string - /** - * Delete line in input - */ - input_delete_line?: string - /** - * Delete to end of line in input - */ - input_delete_to_line_end?: string - /** - * Delete to start of line in input - */ - input_delete_to_line_start?: string - /** - * Backspace in input - */ - input_backspace?: string - /** - * Delete character in input - */ - input_delete?: string - /** - * Undo in input - */ - input_undo?: string - /** - * Redo in input - */ - input_redo?: string - /** - * Move word forward in input - */ - input_word_forward?: string - /** - * Move word backward in input - */ - input_word_backward?: string - /** - * Select word forward in input - */ - input_select_word_forward?: string - /** - * Select word backward in input - */ - input_select_word_backward?: string - /** - * Delete word forward in input - */ - input_delete_word_forward?: string - /** - * Delete word backward in input - */ - input_delete_word_backward?: string - /** - * Previous history item - */ - history_previous?: string - /** - * Next history item - */ - history_next?: string - /** - * Next child session - */ - session_child_cycle?: string - /** - * Previous child session - */ - session_child_cycle_reverse?: string - /** - * Go to parent session - */ - session_parent?: string - /** - * Suspend terminal - */ - terminal_suspend?: string - /** - * Toggle terminal title - */ - terminal_title_toggle?: string - /** - * Toggle tips on home screen - */ - tips_toggle?: string +export type EventServerInstanceDisposed = { + type: "server.instance.disposed" + properties: { + directory: string + } } -/** - * Log level - */ -export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" +export type EventLspClientDiagnostics = { + type: "lsp.client.diagnostics" + properties: { + serverID: string + path: string + } +} -/** - * Server configuration for opencode serve and web commands - */ -export type ServerConfig = { - /** - * Port to listen on - */ - port?: number - /** - * Hostname to listen on - */ - hostname?: string - /** - * Enable mDNS service discovery - */ - mdns?: boolean - /** - * Additional domains to allow for CORS - */ - cors?: Array +export type EventLspUpdated = { + type: "lsp.updated" + properties: { + [key: string]: unknown + } +} + +export type FileDiff = { + file: string + before: string + after: string + additions: number + deletions: number +} + +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { + created: number + } + summary?: { + title?: string + body?: string + diffs: Array + } + agent: string + model: { + providerID: string + modelID: string + } + system?: string + tools?: { + [key: string]: boolean + } + variant?: string } -export type PermissionActionConfig = "ask" | "allow" | "deny" +export type ProviderAuthError = { + name: "ProviderAuthError" + data: { + providerID: string + message: string + } +} -export type PermissionObjectConfig = { - [key: string]: PermissionActionConfig +export type UnknownError = { + name: "UnknownError" + data: { + message: string + } } -export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig +export type MessageOutputLengthError = { + name: "MessageOutputLengthError" + data: { + [key: string]: unknown + } +} -export type PermissionConfig = - | { - __originalKeys?: Array - read?: PermissionRuleConfig - edit?: PermissionRuleConfig - glob?: PermissionRuleConfig - grep?: PermissionRuleConfig - list?: PermissionRuleConfig - bash?: PermissionRuleConfig - task?: PermissionRuleConfig - external_directory?: PermissionRuleConfig - todowrite?: PermissionActionConfig - todoread?: PermissionActionConfig - question?: PermissionActionConfig - webfetch?: PermissionActionConfig - websearch?: PermissionActionConfig - codesearch?: PermissionActionConfig - lsp?: PermissionRuleConfig - doom_loop?: PermissionActionConfig - [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined +export type MessageAbortedError = { + name: "MessageAbortedError" + data: { + message: string + } +} + +export type ApiError = { + name: "APIError" + data: { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string } - | PermissionActionConfig + responseBody?: string + metadata?: { + [key: string]: string + } + } +} -export type AgentConfig = { - model?: string - temperature?: number - top_p?: number - prompt?: string - /** - * @deprecated Use 'permission' field instead - */ - tools?: { - [key: string]: boolean +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { + created: number + completed?: number } - disable?: boolean - /** - * Description of when to use the agent - */ - description?: string - mode?: "subagent" | "primary" | "all" - /** - * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) - */ - hidden?: boolean - options?: { - [key: string]: unknown + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string } - /** - * Hex color code for the agent (e.g., #FF5733) - */ - color?: string - /** - * Maximum number of agentic iterations before forcing text-only response - */ - steps?: number - /** - * @deprecated Use 'steps' field instead. - */ - maxSteps?: number - permission?: PermissionConfig - [key: string]: - | unknown - | string - | number - | { - [key: string]: boolean - } - | boolean - | "subagent" - | "primary" - | "all" - | { - [key: string]: unknown - } - | string - | number - | PermissionConfig - | undefined + summary?: boolean + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + finish?: string } -export type ProviderConfig = { - api?: string - name?: string - env?: Array - id?: string - npm?: string - models?: { - [key: string]: { - id?: string - name?: string - family?: string - release_date?: string - attachment?: boolean - reasoning?: boolean - temperature?: boolean - tool_call?: boolean - interleaved?: - | true - | { - field: "reasoning_content" | "reasoning_details" - } - cost?: { - input: number - output: number - cache_read?: number - cache_write?: number - context_over_200k?: { - input: number - output: number - cache_read?: number - cache_write?: number - } - } - limit?: { - context: number - input?: number - output: number - } - modalities?: { - input: Array<"text" | "audio" | "image" | "video" | "pdf"> - output: Array<"text" | "audio" | "image" | "video" | "pdf"> - } - experimental?: boolean - status?: "alpha" | "beta" | "deprecated" - options?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - provider?: { - npm: string - } - /** - * Variant-specific configuration - */ - variants?: { - [key: string]: { - /** - * Disable this variant for the model - */ - disabled?: boolean - [key: string]: unknown | boolean | undefined - } - } - } +export type Message = UserMessage | AssistantMessage + +export type EventMessageUpdated = { + type: "message.updated" + properties: { + info: Message } - whitelist?: Array - blacklist?: Array - options?: { - apiKey?: string - baseURL?: string - /** - * GitHub Enterprise URL for copilot authentication - */ - enterpriseUrl?: string - /** - * Enable promptCacheKey for this provider (default false) - */ - setCacheKey?: boolean - /** - * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. - */ - timeout?: number | false - [key: string]: unknown | string | boolean | number | false | undefined +} + +export type EventMessageRemoved = { + type: "message.removed" + properties: { + sessionID: string + messageID: string + } +} + +export type TextPart = { + id: string + sessionID: string + messageID: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown } } -export type McpLocalConfig = { - /** - * Type of MCP server connection - */ - type: "local" - /** - * Command and arguments to run the MCP server - */ - command: Array - /** - * Environment variables to set when running the MCP server - */ - environment?: { - [key: string]: string +export type ReasoningPart = { + id: string + sessionID: string + messageID: string + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number } - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number } -export type McpOAuthConfig = { - /** - * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. - */ - clientId?: string - /** - * OAuth client secret (if required by the authorization server) - */ - clientSecret?: string - /** - * OAuth scopes to request during authorization - */ - scope?: string +export type FilePartSourceText = { + value: string + start: number + end: number } -export type McpRemoteConfig = { - /** - * Type of MCP server connection - */ - type: "remote" - /** - * URL of the remote MCP server - */ - url: string - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Headers to send with the request - */ - headers?: { - [key: string]: string +export type FileSource = { + text: FilePartSourceText + type: "file" + path: string +} + +export type Range = { + start: { + line: number + character: number + } + end: { + line: number + character: number } - /** - * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. - */ - oauth?: McpOAuthConfig | false - /** - * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number } -/** - * @deprecated Always uses stretch layout. - */ -export type LayoutConfig = "auto" | "stretch" +export type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: Range + name: string + kind: number +} -export type Config = { - /** - * JSON schema reference for configuration validation - */ - $schema?: string - /** - * Theme name to use for the interface - */ - theme?: string - keybinds?: KeybindsConfig - logLevel?: LogLevel - /** - * TUI specific settings - */ - tui?: { - /** - * TUI scroll speed - */ - scroll_speed?: number - /** - * Scroll acceleration settings - */ - scroll_acceleration?: { - /** - * Enable scroll acceleration - */ - enabled: boolean - } - /** - * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column - */ - diff_style?: "auto" | "stacked" - } - server?: ServerConfig - /** - * Command configuration, see https://opencode.ai/docs/commands - */ - command?: { - [key: string]: { - template: string - description?: string - agent?: string - model?: string - subtask?: boolean - } +export type ResourceSource = { + text: FilePartSourceText + type: "resource" + clientName: string + uri: string +} + +export type FilePartSource = FileSource | SymbolSource | ResourceSource + +export type FilePart = { + id: string + sessionID: string + messageID: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type ToolStatePending = { + status: "pending" + input: { + [key: string]: unknown } - watcher?: { - ignore?: Array + raw: string +} + +export type ToolStateRunning = { + status: "running" + input: { + [key: string]: unknown } - plugin?: Array - snapshot?: boolean - /** - * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing - */ - share?: "manual" | "auto" | "disabled" - /** - * @deprecated Use 'share' field instead. Share newly created sessions automatically - */ - autoshare?: boolean - /** - * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications - */ - autoupdate?: boolean | "notify" - /** - * Disable providers that are loaded automatically - */ - disabled_providers?: Array - /** - * When set, ONLY these providers will be enabled. All other providers will be ignored - */ - enabled_providers?: Array - /** - * Model to use in the format of provider/model, eg anthropic/claude-2 - */ - model?: string - /** - * Small model to use for tasks like title generation in the format of provider/model - */ - small_model?: string - /** - * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. - */ - default_agent?: string - /** - * Custom username to display in conversations instead of system username - */ - username?: string - /** - * @deprecated Use `agent` field instead. - */ - mode?: { - build?: AgentConfig - plan?: AgentConfig - [key: string]: AgentConfig | undefined + title?: string + metadata?: { + [key: string]: unknown } - /** - * Agent configuration, see https://opencode.ai/docs/agent - */ - agent?: { - plan?: AgentConfig - build?: AgentConfig - general?: AgentConfig - explore?: AgentConfig - title?: AgentConfig - summary?: AgentConfig - compaction?: AgentConfig - [key: string]: AgentConfig | undefined + time: { + start: number } - /** - * Custom provider configurations and model overrides - */ - provider?: { - [key: string]: ProviderConfig +} + +export type ToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown } - /** - * MCP (Model Context Protocol) server configurations - */ - mcp?: { - [key: string]: - | McpLocalConfig - | McpRemoteConfig - | { - enabled: boolean - } + output: string + title: string + metadata: { + [key: string]: unknown } - formatter?: - | false - | { - [key: string]: { - disabled?: boolean - command?: Array - environment?: { - [key: string]: string - } - extensions?: Array - } - } - lsp?: - | false - | { - [key: string]: - | { - disabled: true - } - | { - command: Array - extensions?: Array - disabled?: boolean - env?: { - [key: string]: string - } - initialization?: { - [key: string]: unknown - } - } - } - /** - * Additional instruction files or patterns to include - */ - instructions?: Array - layout?: LayoutConfig - permission?: PermissionConfig - tools?: { - [key: string]: boolean + time: { + start: number + end: number + compacted?: number } - enterprise?: { - /** - * Enterprise URL - */ - url?: string + attachments?: Array +} + +export type ToolStateError = { + status: "error" + input: { + [key: string]: unknown } - compaction?: { - /** - * Enable automatic compaction when context is full (default: true) - */ - auto?: boolean - /** - * Enable pruning of old tool outputs (default: true) - */ - prune?: boolean + error: string + metadata?: { + [key: string]: unknown } - experimental?: { - hook?: { - file_edited?: { - [key: string]: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> - } - session_completed?: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> - } - /** - * Number of retries for chat completions on failure - */ - chatMaxRetries?: number - disable_paste_summary?: boolean - /** - * Enable the batch tool - */ - batch_tool?: boolean - /** - * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) - */ - openTelemetry?: boolean - /** - * Tools that should only be available to primary agents. - */ - primary_tools?: Array - /** - * Continue the agent loop when a tool call is denied - */ - continue_loop_on_deny?: boolean - /** - * Timeout in milliseconds for model context protocol (MCP) requests - */ - mcp_timeout?: number + time: { + start: number + end: number } } -export type EventConfigUpdated = { - type: "config.updated" - properties: Config -} +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError -export type EventServerInstanceDisposed = { - type: "server.instance.disposed" - properties: { - directory: string +export type ToolPart = { + id: string + sessionID: string + messageID: string + type: "tool" + callID: string + tool: string + state: ToolState + metadata?: { + [key: string]: unknown } } -export type EventLspClientDiagnostics = { - type: "lsp.client.diagnostics" - properties: { - serverID: string - path: string - } +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" + snapshot?: string } -export type EventLspUpdated = { - type: "lsp.updated" - properties: { - [key: string]: unknown +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + reason: string + snapshot?: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } } } -export type FileDiff = { - file: string - before: string - after: string - additions: number - deletions: number +export type SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string } -export type UserMessage = { +export type PatchPart = { id: string sessionID: string - role: "user" + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type RetryPart = { + id: string + sessionID: string + messageID: string + type: "retry" + attempt: number + error: ApiError time: { created: number } - summary?: { - title?: string - body?: string - diffs: Array - } - agent: string - model: { - providerID: string - modelID: string - } - system?: string - tools?: { - [key: string]: boolean - } - variant?: string } -export type ProviderAuthError = { - name: "ProviderAuthError" - data: { - providerID: string - message: string +export type CompactionPart = { + id: string + sessionID: string + messageID: string + type: "compaction" + auto: boolean +} + +export type Part = + | TextPart + | { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + command?: string + } + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | RetryPart + | CompactionPart + +export type EventMessagePartUpdated = { + type: "message.part.updated" + properties: { + part: Part + delta?: string } } -export type UnknownError = { - name: "UnknownError" - data: { - message: string +export type EventMessagePartRemoved = { + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string } } -export type MessageOutputLengthError = { - name: "MessageOutputLengthError" - data: { +export type PermissionRequest = { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { [key: string]: unknown } + always: Array + tool?: { + messageID: string + callID: string + } } -export type MessageAbortedError = { - name: "MessageAbortedError" - data: { - message: string - } +export type EventPermissionAsked = { + type: "permission.asked" + properties: PermissionRequest } -export type ApiError = { - name: "APIError" - data: { - message: string - statusCode?: number - isRetryable: boolean - responseHeaders?: { - [key: string]: string - } - responseBody?: string - metadata?: { - [key: string]: string - } +export type EventPermissionReplied = { + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" } } -export type AssistantMessage = { - id: string - sessionID: string - role: "assistant" - time: { - created: number - completed?: number - } - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError - parentID: string - modelID: string - providerID: string - mode: string - agent: string - path: { - cwd: string - root: string - } - summary?: boolean - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number +export type SessionStatus = + | { + type: "idle" + } + | { + type: "retry" + attempt: number + message: string + next: number + } + | { + type: "busy" } + +export type EventSessionStatus = { + type: "session.status" + properties: { + sessionID: string + status: SessionStatus } - finish?: string } -export type Message = UserMessage | AssistantMessage - -export type EventMessageUpdated = { - type: "message.updated" +export type EventSessionIdle = { + type: "session.idle" properties: { - info: Message + sessionID: string } } -export type EventMessageRemoved = { - type: "message.removed" +export type EventSessionCompacted = { + type: "session.compacted" properties: { sessionID: string - messageID: string } } -export type TextPart = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string } } -export type ReasoningPart = { +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string + /** + * Unique identifier for the todo item + */ id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end?: number +} + +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array } } -export type FilePartSourceText = { - value: string - start: number - end: number +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } } -export type FileSource = { - text: FilePartSourceText - type: "file" - path: string +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } } -export type Range = { - start: { - line: number - character: number +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number } - end: { - line: number - character: number +} + +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string } } -export type SymbolSource = { - text: FilePartSourceText - type: "symbol" - path: string - range: Range - name: string - kind: number +export type EventMcpToolsChanged = { + type: "mcp.tools.changed" + properties: { + server: string + } } -export type ResourceSource = { - text: FilePartSourceText - type: "resource" - clientName: string - uri: string +export type EventCommandExecuted = { + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } } -export type FilePartSource = FileSource | SymbolSource | ResourceSource +export type PermissionAction = "allow" | "deny" | "ask" -export type FilePart = { - id: string - sessionID: string - messageID: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource +export type PermissionRule = { + permission: string + pattern: string + action: PermissionAction } -export type ToolStatePending = { - status: "pending" - input: { - [key: string]: unknown - } - raw: string -} +export type PermissionRuleset = Array -export type ToolStateRunning = { - status: "running" - input: { - [key: string]: unknown +export type Session = { + id: string + projectID: string + directory: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array } - title?: string - metadata?: { - [key: string]: unknown + share?: { + url: string } + title: string + version: string time: { - start: number + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string } } -export type ToolStateCompleted = { - status: "completed" - input: { - [key: string]: unknown - } - output: string - title: string - metadata: { - [key: string]: unknown - } - time: { - start: number - end: number - compacted?: number +export type EventSessionCreated = { + type: "session.created" + properties: { + info: Session } - attachments?: Array } -export type ToolStateError = { - status: "error" - input: { - [key: string]: unknown - } - error: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end: number +export type EventSessionUpdated = { + type: "session.updated" + properties: { + info: Session } } -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - -export type ToolPart = { - id: string - sessionID: string - messageID: string - type: "tool" - callID: string - tool: string - state: ToolState - metadata?: { - [key: string]: unknown +export type EventSessionDeleted = { + type: "session.deleted" + properties: { + info: Session } } -export type StepStartPart = { - id: string - sessionID: string - messageID: string - type: "step-start" - snapshot?: string +export type EventSessionDiff = { + type: "session.diff" + properties: { + sessionID: string + diff: Array + } } -export type StepFinishPart = { - id: string - sessionID: string - messageID: string - type: "step-finish" - reason: string - snapshot?: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } +export type EventSessionError = { + type: "session.error" + properties: { + sessionID?: string + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError } } -export type SnapshotPart = { - id: string - sessionID: string - messageID: string - type: "snapshot" - snapshot: string +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } } -export type PatchPart = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" + properties: { + branch?: string + } } -export type AgentPart = { +export type Pty = { id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number } -export type RetryPart = { - id: string - sessionID: string - messageID: string - type: "retry" - attempt: number - error: ApiError - time: { - created: number +export type EventPtyCreated = { + type: "pty.created" + properties: { + info: Pty } } -export type CompactionPart = { - id: string - sessionID: string - messageID: string - type: "compaction" - auto: boolean +export type EventPtyUpdated = { + type: "pty.updated" + properties: { + info: Pty + } } -export type Part = - | TextPart - | { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - command?: string - } - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - -export type EventMessagePartUpdated = { - type: "message.part.updated" +export type EventPtyExited = { + type: "pty.exited" properties: { - part: Part - delta?: string + id: string + exitCode: number } } -export type EventMessagePartRemoved = { - type: "message.part.removed" +export type EventPtyDeleted = { + type: "pty.deleted" properties: { - sessionID: string - messageID: string - partID: string + id: string } } -export type PermissionRequest = { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { +export type EventServerConnected = { + type: "server.connected" + properties: { [key: string]: unknown } - always: Array - tool?: { - messageID: string - callID: string - } -} - -export type EventPermissionAsked = { - type: "permission.asked" - properties: PermissionRequest } -export type EventPermissionReplied = { - type: "permission.replied" +export type EventGlobalDisposed = { + type: "global.disposed" properties: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" + [key: string]: unknown } } -export type SessionStatus = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - next: number - } - | { - type: "busy" - } +export type Event = + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventProjectUpdated + | EventServerInstanceDisposed + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventPermissionAsked + | EventPermissionReplied + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventFileEdited + | EventTodoUpdated + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventCommandExecuted + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionDiff + | EventSessionError + | EventFileWatcherUpdated + | EventVcsBranchUpdated + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventServerConnected + | EventGlobalDisposed -export type EventSessionStatus = { - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } +export type GlobalEvent = { + directory: string + payload: Event } -export type EventSessionIdle = { - type: "session.idle" - properties: { - sessionID: string +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string } } -export type QuestionOption = { +/** + * Custom keybind configurations + */ +export type KeybindsConfig = { + /** + * Leader key for keybind combinations + */ + leader?: string + /** + * Exit the application + */ + app_exit?: string + /** + * Open external editor + */ + editor_open?: string + /** + * List available themes + */ + theme_list?: string + /** + * Toggle sidebar + */ + sidebar_toggle?: string + /** + * Toggle session scrollbar + */ + scrollbar_toggle?: string + /** + * Toggle username visibility + */ + username_toggle?: string + /** + * View status + */ + status_view?: string + /** + * Export session to editor + */ + session_export?: string + /** + * Create a new session + */ + session_new?: string + /** + * List all sessions + */ + session_list?: string + /** + * Show session timeline + */ + session_timeline?: string + /** + * Fork session from message + */ + session_fork?: string + /** + * Rename session + */ + session_rename?: string + /** + * Share current session + */ + session_share?: string + /** + * Unshare current session + */ + session_unshare?: string + /** + * Interrupt current session + */ + session_interrupt?: string + /** + * Compact the session + */ + session_compact?: string + /** + * Scroll messages up by one page + */ + messages_page_up?: string + /** + * Scroll messages down by one page + */ + messages_page_down?: string + /** + * Scroll messages up by half page + */ + messages_half_page_up?: string + /** + * Scroll messages down by half page + */ + messages_half_page_down?: string + /** + * Navigate to first message + */ + messages_first?: string + /** + * Navigate to last message + */ + messages_last?: string + /** + * Navigate to next message + */ + messages_next?: string + /** + * Navigate to previous message + */ + messages_previous?: string + /** + * Navigate to last user message + */ + messages_last_user?: string + /** + * Copy message + */ + messages_copy?: string + /** + * Undo message + */ + messages_undo?: string + /** + * Redo message + */ + messages_redo?: string + /** + * Toggle code block concealment in messages + */ + messages_toggle_conceal?: string + /** + * Toggle tool details visibility + */ + tool_details?: string + /** + * List available models + */ + model_list?: string + /** + * Next recently used model + */ + model_cycle_recent?: string + /** + * Previous recently used model + */ + model_cycle_recent_reverse?: string + /** + * Next favorite model + */ + model_cycle_favorite?: string + /** + * Previous favorite model + */ + model_cycle_favorite_reverse?: string /** - * Display text (1-5 words, concise) + * List available commands */ - label: string + command_list?: string /** - * Explanation of choice + * List agents */ - description: string -} - -export type QuestionInfo = { + agent_list?: string /** - * Complete question + * Next agent */ - question: string + agent_cycle?: string /** - * Very short label (max 12 chars) + * Previous agent */ - header: string + agent_cycle_reverse?: string /** - * Available choices + * Cycle model variants */ - options: Array + variant_cycle?: string /** - * Allow selecting multiple choices + * Clear input field */ - multiple?: boolean + input_clear?: string /** - * Allow typing a custom answer (default: true) + * Paste from clipboard */ - custom?: boolean -} - -export type QuestionRequest = { - id: string - sessionID: string + input_paste?: string /** - * Questions to ask + * Submit input */ - questions: Array - tool?: { - messageID: string - callID: string - } + input_submit?: string + /** + * Insert newline in input + */ + input_newline?: string + /** + * Move cursor left in input + */ + input_move_left?: string + /** + * Move cursor right in input + */ + input_move_right?: string + /** + * Move cursor up in input + */ + input_move_up?: string + /** + * Move cursor down in input + */ + input_move_down?: string + /** + * Select left in input + */ + input_select_left?: string + /** + * Select right in input + */ + input_select_right?: string + /** + * Select up in input + */ + input_select_up?: string + /** + * Select down in input + */ + input_select_down?: string + /** + * Move to start of line in input + */ + input_line_home?: string + /** + * Move to end of line in input + */ + input_line_end?: string + /** + * Select to start of line in input + */ + input_select_line_home?: string + /** + * Select to end of line in input + */ + input_select_line_end?: string + /** + * Move to start of visual line in input + */ + input_visual_line_home?: string + /** + * Move to end of visual line in input + */ + input_visual_line_end?: string + /** + * Select to start of visual line in input + */ + input_select_visual_line_home?: string + /** + * Select to end of visual line in input + */ + input_select_visual_line_end?: string + /** + * Move to start of buffer in input + */ + input_buffer_home?: string + /** + * Move to end of buffer in input + */ + input_buffer_end?: string + /** + * Select to start of buffer in input + */ + input_select_buffer_home?: string + /** + * Select to end of buffer in input + */ + input_select_buffer_end?: string + /** + * Delete line in input + */ + input_delete_line?: string + /** + * Delete to end of line in input + */ + input_delete_to_line_end?: string + /** + * Delete to start of line in input + */ + input_delete_to_line_start?: string + /** + * Backspace in input + */ + input_backspace?: string + /** + * Delete character in input + */ + input_delete?: string + /** + * Undo in input + */ + input_undo?: string + /** + * Redo in input + */ + input_redo?: string + /** + * Move word forward in input + */ + input_word_forward?: string + /** + * Move word backward in input + */ + input_word_backward?: string + /** + * Select word forward in input + */ + input_select_word_forward?: string + /** + * Select word backward in input + */ + input_select_word_backward?: string + /** + * Delete word forward in input + */ + input_delete_word_forward?: string + /** + * Delete word backward in input + */ + input_delete_word_backward?: string + /** + * Previous history item + */ + history_previous?: string + /** + * Next history item + */ + history_next?: string + /** + * Next child session + */ + session_child_cycle?: string + /** + * Previous child session + */ + session_child_cycle_reverse?: string + /** + * Go to parent session + */ + session_parent?: string + /** + * Suspend terminal + */ + terminal_suspend?: string + /** + * Toggle terminal title + */ + terminal_title_toggle?: string + /** + * Toggle tips on home screen + */ + tips_toggle?: string } -export type EventQuestionAsked = { - type: "question.asked" - properties: QuestionRequest +/** + * Log level + */ +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" + +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { + /** + * Port to listen on + */ + port?: number + /** + * Hostname to listen on + */ + hostname?: string + /** + * Enable mDNS service discovery + */ + mdns?: boolean + /** + * Additional domains to allow for CORS + */ + cors?: Array } -export type QuestionAnswer = Array +export type PermissionActionConfig = "ask" | "allow" | "deny" -export type EventQuestionReplied = { - type: "question.replied" - properties: { - sessionID: string - requestID: string - answers: Array - } +export type PermissionObjectConfig = { + [key: string]: PermissionActionConfig } -export type EventQuestionRejected = { - type: "question.rejected" - properties: { - sessionID: string - requestID: string - } -} +export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig -export type EventSessionCompacted = { - type: "session.compacted" - properties: { - sessionID: string - } -} +export type PermissionConfig = + | { + __originalKeys?: Array + read?: PermissionRuleConfig + edit?: PermissionRuleConfig + glob?: PermissionRuleConfig + grep?: PermissionRuleConfig + list?: PermissionRuleConfig + bash?: PermissionRuleConfig + task?: PermissionRuleConfig + external_directory?: PermissionRuleConfig + todowrite?: PermissionActionConfig + todoread?: PermissionActionConfig + webfetch?: PermissionActionConfig + websearch?: PermissionActionConfig + codesearch?: PermissionActionConfig + lsp?: PermissionRuleConfig + doom_loop?: PermissionActionConfig + [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined + } + | PermissionActionConfig -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string +export type AgentConfig = { + model?: string + temperature?: number + top_p?: number + prompt?: string + /** + * @deprecated Use 'permission' field instead + */ + tools?: { + [key: string]: boolean } -} - -export type Todo = { + disable?: boolean /** - * Brief description of the task + * Description of when to use the agent */ - content: string + description?: string + mode?: "subagent" | "primary" | "all" /** - * Current status of the task: pending, in_progress, completed, cancelled + * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) */ - status: string + hidden?: boolean + options?: { + [key: string]: unknown + } /** - * Priority level of the task: high, medium, low + * Hex color code for the agent (e.g., #FF5733) */ - priority: string + color?: string /** - * Unique identifier for the todo item + * Maximum number of agentic iterations before forcing text-only response */ - id: string -} - -export type EventTodoUpdated = { - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } + steps?: number + /** + * @deprecated Use 'steps' field instead. + */ + maxSteps?: number + permission?: PermissionConfig + [key: string]: + | unknown + | string + | number + | { + [key: string]: boolean + } + | boolean + | "subagent" + | "primary" + | "all" + | { + [key: string]: unknown + } + | string + | number + | PermissionConfig + | undefined } -export type EventSkillUpdated = { - type: "skill.updated" - properties: { +export type ProviderConfig = { + api?: string + name?: string + env?: Array + id?: string + npm?: string + models?: { [key: string]: { - name: string - description: string - location: string - } - } -} - -export type EventTuiPromptAppend = { - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow = { - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ - duration?: number - } -} - -export type EventTuiSessionSelect = { - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type EventMcpToolsChanged = { - type: "mcp.tools.changed" - properties: { - server: string - } -} - -export type EventCommandUpdated = { - type: "command.updated" - properties: { - [key: string]: unknown - } -} - -export type EventCommandExecuted = { - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - -export type PermissionAction = "allow" | "deny" | "ask" - -export type PermissionRule = { - permission: string - pattern: string - action: PermissionAction -} - -export type PermissionRuleset = Array - -export type Session = { - id: string - slug: string - projectID: string - directory: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - share?: { - url: string - } - title: string - version: string - time: { - created: number - updated: number - compacting?: number - archived?: number + id?: string + name?: string + family?: string + release_date?: string + attachment?: boolean + reasoning?: boolean + temperature?: boolean + tool_call?: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit?: { + context: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + options?: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + provider?: { + npm: string + } + /** + * Variant-specific configuration + */ + variants?: { + [key: string]: { + /** + * Disable this variant for the model + */ + disabled?: boolean + [key: string]: unknown | boolean | undefined + } + } + } } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string + whitelist?: Array + blacklist?: Array + options?: { + apiKey?: string + baseURL?: string + /** + * GitHub Enterprise URL for copilot authentication + */ + enterpriseUrl?: string + /** + * Enable promptCacheKey for this provider (default false) + */ + setCacheKey?: boolean + /** + * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. + */ + timeout?: number | false + [key: string]: unknown | string | boolean | number | false | undefined } } -export type EventSessionCreated = { - type: "session.created" - properties: { - info: Session +export type McpLocalConfig = { + /** + * Type of MCP server connection + */ + type: "local" + /** + * Command and arguments to run the MCP server + */ + command: Array + /** + * Environment variables to set when running the MCP server + */ + environment?: { + [key: string]: string } + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number } -export type EventSessionUpdated = { - type: "session.updated" - properties: { - info: Session - } +export type McpOAuthConfig = { + /** + * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. + */ + clientId?: string + /** + * OAuth client secret (if required by the authorization server) + */ + clientSecret?: string + /** + * OAuth scopes to request during authorization + */ + scope?: string } -export type EventSessionDeleted = { - type: "session.deleted" - properties: { - info: Session +export type McpRemoteConfig = { + /** + * Type of MCP server connection + */ + type: "remote" + /** + * URL of the remote MCP server + */ + url: string + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Headers to send with the request + */ + headers?: { + [key: string]: string } + /** + * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. + */ + oauth?: McpOAuthConfig | false + /** + * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number } -export type EventSessionDiff = { - type: "session.diff" - properties: { - sessionID: string - diff: Array - } -} +/** + * @deprecated Always uses stretch layout. + */ +export type LayoutConfig = "auto" | "stretch" -export type EventSessionError = { - type: "session.error" - properties: { - sessionID?: string - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError +export type Config = { + /** + * JSON schema reference for configuration validation + */ + $schema?: string + /** + * Theme name to use for the interface + */ + theme?: string + keybinds?: KeybindsConfig + logLevel?: LogLevel + /** + * TUI specific settings + */ + tui?: { + /** + * TUI scroll speed + */ + scroll_speed?: number + /** + * Scroll acceleration settings + */ + scroll_acceleration?: { + /** + * Enable scroll acceleration + */ + enabled: boolean + } + /** + * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column + */ + diff_style?: "auto" | "stacked" } -} - -export type EventVcsBranchUpdated = { - type: "vcs.branch.updated" - properties: { - branch?: string + server?: ServerConfig + /** + * Command configuration, see https://opencode.ai/docs/commands + */ + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + subtask?: boolean + } } -} - -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number -} - -export type EventPtyCreated = { - type: "pty.created" - properties: { - info: Pty + watcher?: { + ignore?: Array } -} - -export type EventPtyUpdated = { - type: "pty.updated" - properties: { - info: Pty + plugin?: Array + snapshot?: boolean + /** + * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing + */ + share?: "manual" | "auto" | "disabled" + /** + * @deprecated Use 'share' field instead. Share newly created sessions automatically + */ + autoshare?: boolean + /** + * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications + */ + autoupdate?: boolean | "notify" + /** + * Disable providers that are loaded automatically + */ + disabled_providers?: Array + /** + * When set, ONLY these providers will be enabled. All other providers will be ignored + */ + enabled_providers?: Array + /** + * Model to use in the format of provider/model, eg anthropic/claude-2 + */ + model?: string + /** + * Small model to use for tasks like title generation in the format of provider/model + */ + small_model?: string + /** + * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. + */ + default_agent?: string + /** + * Custom username to display in conversations instead of system username + */ + username?: string + /** + * @deprecated Use `agent` field instead. + */ + mode?: { + build?: AgentConfig + plan?: AgentConfig + [key: string]: AgentConfig | undefined } -} - -export type EventPtyExited = { - type: "pty.exited" - properties: { - id: string - exitCode: number + /** + * Agent configuration, see https://opencode.ai/docs/agent + */ + agent?: { + plan?: AgentConfig + build?: AgentConfig + general?: AgentConfig + explore?: AgentConfig + title?: AgentConfig + summary?: AgentConfig + compaction?: AgentConfig + [key: string]: AgentConfig | undefined + } + /** + * Custom provider configurations and model overrides + */ + provider?: { + [key: string]: ProviderConfig } -} - -export type EventPtyDeleted = { - type: "pty.deleted" - properties: { - id: string + /** + * MCP (Model Context Protocol) server configurations + */ + mcp?: { + [key: string]: + | McpLocalConfig + | McpRemoteConfig + | { + enabled: boolean + } } -} - -export type EventServerConnected = { - type: "server.connected" - properties: { - [key: string]: unknown + formatter?: + | false + | { + [key: string]: { + disabled?: boolean + command?: Array + environment?: { + [key: string]: string + } + extensions?: Array + } + } + lsp?: + | false + | { + [key: string]: + | { + disabled: true + } + | { + command: Array + extensions?: Array + disabled?: boolean + env?: { + [key: string]: string + } + initialization?: { + [key: string]: unknown + } + } + } + /** + * Additional instruction files or patterns to include + */ + instructions?: Array + layout?: LayoutConfig + permission?: PermissionConfig + tools?: { + [key: string]: boolean } -} - -export type EventGlobalDisposed = { - type: "global.disposed" - properties: { - [key: string]: unknown + enterprise?: { + /** + * Enterprise URL + */ + url?: string } -} - -export type Event = - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventProjectUpdated - | EventConfigUpdated - | EventServerInstanceDisposed - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventPermissionAsked - | EventPermissionReplied - | EventSessionStatus - | EventSessionIdle - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventSessionCompacted - | EventFileEdited - | EventTodoUpdated - | EventFileWatcherUpdated - | EventSkillUpdated - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventCommandUpdated - | EventCommandExecuted - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | EventSessionDiff - | EventSessionError - | EventVcsBranchUpdated - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventServerConnected - | EventGlobalDisposed - -export type GlobalEvent = { - directory: string - payload: Event -} - -export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false -} - -export type NotFoundError = { - name: "NotFoundError" - data: { - message: string + compaction?: { + /** + * Enable automatic compaction when context is full (default: true) + */ + auto?: boolean + /** + * Enable pruning of old tool outputs (default: true) + */ + prune?: boolean + } + experimental?: { + hook?: { + file_edited?: { + [key: string]: Array<{ + command: Array + environment?: { + [key: string]: string + } + }> + } + session_completed?: Array<{ + command: Array + environment?: { + [key: string]: string + } + }> + } + /** + * Number of retries for chat completions on failure + */ + chatMaxRetries?: number + disable_paste_summary?: boolean + /** + * Enable the batch tool + */ + batch_tool?: boolean + /** + * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) + */ + openTelemetry?: boolean + /** + * Tools that should only be available to primary agents. + */ + primary_tools?: Array + /** + * Continue the agent loop when a tool call is denied + */ + continue_loop_on_deny?: boolean + /** + * Timeout in milliseconds for model context protocol (MCP) requests + */ + mcp_timeout?: number } } @@ -1946,7 +1827,6 @@ export type Model = { } limit: { context: number - input?: number output: number } status: "alpha" | "beta" | "deprecated" | "active" @@ -2111,7 +1991,6 @@ export type OAuth = { refresh: string access: string expires: number - accountId?: string enterpriseUrl?: string } @@ -2633,14 +2512,7 @@ export type SessionListData = { body?: never path?: never query?: { - /** - * Filter sessions by project directory - */ directory?: string - /** - * Only return root sessions (no parentID) - */ - roots?: boolean /** * Filter sessions updated on or after this timestamp (milliseconds since epoch) */ @@ -3673,95 +3545,6 @@ export type PermissionListResponses = { export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] -export type QuestionListData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/question" -} - -export type QuestionListResponses = { - /** - * List of pending questions - */ - 200: Array -} - -export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] - -export type QuestionReplyData = { - body?: { - /** - * User answers in order of questions (each answer is an array of selected labels) - */ - answers: Array - } - path: { - requestID: string - } - query?: { - directory?: string - } - url: "/question/{requestID}/reply" -} - -export type QuestionReplyErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors] - -export type QuestionReplyResponses = { - /** - * Question answered successfully - */ - 200: boolean -} - -export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] - -export type QuestionRejectData = { - body?: never - path: { - requestID: string - } - query?: { - directory?: string - } - url: "/question/{requestID}/reject" -} - -export type QuestionRejectErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors] - -export type QuestionRejectResponses = { - /** - * Question rejected successfully - */ - 200: boolean -} - -export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses] - export type CommandListData = { body?: never path?: never @@ -3852,7 +3635,6 @@ export type ProviderListResponses = { } limit: { context: number - input?: number output: number } modalities?: { From 762599b0aff24514501ee1b94552428b6c9320b9 Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:41:16 +0000 Subject: [PATCH 25/27] Revert "chore(sdk): align generated types with dev" This reverts commit e4cb7cc2356ae93fdcc3bcb4f4f447c7d1466070. --- packages/sdk/js/src/v2/gen/types.gen.ts | 2894 ++++++++++++----------- 1 file changed, 1556 insertions(+), 1338 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 97a695162ed..d198027633b 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -40,797 +40,6 @@ export type EventProjectUpdated = { properties: Project } -export type EventServerInstanceDisposed = { - type: "server.instance.disposed" - properties: { - directory: string - } -} - -export type EventLspClientDiagnostics = { - type: "lsp.client.diagnostics" - properties: { - serverID: string - path: string - } -} - -export type EventLspUpdated = { - type: "lsp.updated" - properties: { - [key: string]: unknown - } -} - -export type FileDiff = { - file: string - before: string - after: string - additions: number - deletions: number -} - -export type UserMessage = { - id: string - sessionID: string - role: "user" - time: { - created: number - } - summary?: { - title?: string - body?: string - diffs: Array - } - agent: string - model: { - providerID: string - modelID: string - } - system?: string - tools?: { - [key: string]: boolean - } - variant?: string -} - -export type ProviderAuthError = { - name: "ProviderAuthError" - data: { - providerID: string - message: string - } -} - -export type UnknownError = { - name: "UnknownError" - data: { - message: string - } -} - -export type MessageOutputLengthError = { - name: "MessageOutputLengthError" - data: { - [key: string]: unknown - } -} - -export type MessageAbortedError = { - name: "MessageAbortedError" - data: { - message: string - } -} - -export type ApiError = { - name: "APIError" - data: { - message: string - statusCode?: number - isRetryable: boolean - responseHeaders?: { - [key: string]: string - } - responseBody?: string - metadata?: { - [key: string]: string - } - } -} - -export type AssistantMessage = { - id: string - sessionID: string - role: "assistant" - time: { - created: number - completed?: number - } - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError - parentID: string - modelID: string - providerID: string - mode: string - agent: string - path: { - cwd: string - root: string - } - summary?: boolean - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } - finish?: string -} - -export type Message = UserMessage | AssistantMessage - -export type EventMessageUpdated = { - type: "message.updated" - properties: { - info: Message - } -} - -export type EventMessageRemoved = { - type: "message.removed" - properties: { - sessionID: string - messageID: string - } -} - -export type TextPart = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown - } -} - -export type ReasoningPart = { - id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end?: number - } -} - -export type FilePartSourceText = { - value: string - start: number - end: number -} - -export type FileSource = { - text: FilePartSourceText - type: "file" - path: string -} - -export type Range = { - start: { - line: number - character: number - } - end: { - line: number - character: number - } -} - -export type SymbolSource = { - text: FilePartSourceText - type: "symbol" - path: string - range: Range - name: string - kind: number -} - -export type ResourceSource = { - text: FilePartSourceText - type: "resource" - clientName: string - uri: string -} - -export type FilePartSource = FileSource | SymbolSource | ResourceSource - -export type FilePart = { - id: string - sessionID: string - messageID: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource -} - -export type ToolStatePending = { - status: "pending" - input: { - [key: string]: unknown - } - raw: string -} - -export type ToolStateRunning = { - status: "running" - input: { - [key: string]: unknown - } - title?: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - } -} - -export type ToolStateCompleted = { - status: "completed" - input: { - [key: string]: unknown - } - output: string - title: string - metadata: { - [key: string]: unknown - } - time: { - start: number - end: number - compacted?: number - } - attachments?: Array -} - -export type ToolStateError = { - status: "error" - input: { - [key: string]: unknown - } - error: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end: number - } -} - -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - -export type ToolPart = { - id: string - sessionID: string - messageID: string - type: "tool" - callID: string - tool: string - state: ToolState - metadata?: { - [key: string]: unknown - } -} - -export type StepStartPart = { - id: string - sessionID: string - messageID: string - type: "step-start" - snapshot?: string -} - -export type StepFinishPart = { - id: string - sessionID: string - messageID: string - type: "step-finish" - reason: string - snapshot?: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } - } -} - -export type SnapshotPart = { - id: string - sessionID: string - messageID: string - type: "snapshot" - snapshot: string -} - -export type PatchPart = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array -} - -export type AgentPart = { - id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number - } -} - -export type RetryPart = { - id: string - sessionID: string - messageID: string - type: "retry" - attempt: number - error: ApiError - time: { - created: number - } -} - -export type CompactionPart = { - id: string - sessionID: string - messageID: string - type: "compaction" - auto: boolean -} - -export type Part = - | TextPart - | { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - command?: string - } - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - -export type EventMessagePartUpdated = { - type: "message.part.updated" - properties: { - part: Part - delta?: string - } -} - -export type EventMessagePartRemoved = { - type: "message.part.removed" - properties: { - sessionID: string - messageID: string - partID: string - } -} - -export type PermissionRequest = { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string - } -} - -export type EventPermissionAsked = { - type: "permission.asked" - properties: PermissionRequest -} - -export type EventPermissionReplied = { - type: "permission.replied" - properties: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" - } -} - -export type SessionStatus = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - next: number - } - | { - type: "busy" - } - -export type EventSessionStatus = { - type: "session.status" - properties: { - sessionID: string - status: SessionStatus - } -} - -export type EventSessionIdle = { - type: "session.idle" - properties: { - sessionID: string - } -} - -export type EventSessionCompacted = { - type: "session.compacted" - properties: { - sessionID: string - } -} - -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string - } -} - -export type Todo = { - /** - * Brief description of the task - */ - content: string - /** - * Current status of the task: pending, in_progress, completed, cancelled - */ - status: string - /** - * Priority level of the task: high, medium, low - */ - priority: string - /** - * Unique identifier for the todo item - */ - id: string -} - -export type EventTodoUpdated = { - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - -export type EventTuiPromptAppend = { - type: "tui.prompt.append" - properties: { - text: string - } -} - -export type EventTuiCommandExecute = { - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } -} - -export type EventTuiToastShow = { - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ - duration?: number - } -} - -export type EventTuiSessionSelect = { - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} - -export type EventMcpToolsChanged = { - type: "mcp.tools.changed" - properties: { - server: string - } -} - -export type EventCommandExecuted = { - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - -export type PermissionAction = "allow" | "deny" | "ask" - -export type PermissionRule = { - permission: string - pattern: string - action: PermissionAction -} - -export type PermissionRuleset = Array - -export type Session = { - id: string - projectID: string - directory: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - share?: { - url: string - } - title: string - version: string - time: { - created: number - updated: number - compacting?: number - archived?: number - } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string - } -} - -export type EventSessionCreated = { - type: "session.created" - properties: { - info: Session - } -} - -export type EventSessionUpdated = { - type: "session.updated" - properties: { - info: Session - } -} - -export type EventSessionDeleted = { - type: "session.deleted" - properties: { - info: Session - } -} - -export type EventSessionDiff = { - type: "session.diff" - properties: { - sessionID: string - diff: Array - } -} - -export type EventSessionError = { - type: "session.error" - properties: { - sessionID?: string - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError - } -} - -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - -export type EventVcsBranchUpdated = { - type: "vcs.branch.updated" - properties: { - branch?: string - } -} - -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number -} - -export type EventPtyCreated = { - type: "pty.created" - properties: { - info: Pty - } -} - -export type EventPtyUpdated = { - type: "pty.updated" - properties: { - info: Pty - } -} - -export type EventPtyExited = { - type: "pty.exited" - properties: { - id: string - exitCode: number - } -} - -export type EventPtyDeleted = { - type: "pty.deleted" - properties: { - id: string - } -} - -export type EventServerConnected = { - type: "server.connected" - properties: { - [key: string]: unknown - } -} - -export type EventGlobalDisposed = { - type: "global.disposed" - properties: { - [key: string]: unknown - } -} - -export type Event = - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventProjectUpdated - | EventServerInstanceDisposed - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventPermissionAsked - | EventPermissionReplied - | EventSessionStatus - | EventSessionIdle - | EventSessionCompacted - | EventFileEdited - | EventTodoUpdated - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventCommandExecuted - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | EventSessionDiff - | EventSessionError - | EventFileWatcherUpdated - | EventVcsBranchUpdated - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventServerConnected - | EventGlobalDisposed - -export type GlobalEvent = { - directory: string - payload: Event -} - -export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false -} - -export type NotFoundError = { - name: "NotFoundError" - data: { - message: string - } -} - /** * Custom keybind configurations */ @@ -891,6 +100,22 @@ export type KeybindsConfig = { * Rename session */ session_rename?: string + /** + * Delete session + */ + session_delete?: string + /** + * Delete stash entry + */ + stash_delete?: string + /** + * Open provider list from model dialog + */ + model_provider_list?: string + /** + * Toggle model favorite status + */ + model_favorite_toggle?: string /** * Share current session */ @@ -1004,685 +229,1579 @@ export type KeybindsConfig = { */ variant_cycle?: string /** - * Clear input field + * Clear input field + */ + input_clear?: string + /** + * Paste from clipboard + */ + input_paste?: string + /** + * Submit input + */ + input_submit?: string + /** + * Insert newline in input + */ + input_newline?: string + /** + * Move cursor left in input + */ + input_move_left?: string + /** + * Move cursor right in input + */ + input_move_right?: string + /** + * Move cursor up in input + */ + input_move_up?: string + /** + * Move cursor down in input + */ + input_move_down?: string + /** + * Select left in input + */ + input_select_left?: string + /** + * Select right in input + */ + input_select_right?: string + /** + * Select up in input + */ + input_select_up?: string + /** + * Select down in input + */ + input_select_down?: string + /** + * Move to start of line in input + */ + input_line_home?: string + /** + * Move to end of line in input + */ + input_line_end?: string + /** + * Select to start of line in input + */ + input_select_line_home?: string + /** + * Select to end of line in input + */ + input_select_line_end?: string + /** + * Move to start of visual line in input + */ + input_visual_line_home?: string + /** + * Move to end of visual line in input + */ + input_visual_line_end?: string + /** + * Select to start of visual line in input + */ + input_select_visual_line_home?: string + /** + * Select to end of visual line in input + */ + input_select_visual_line_end?: string + /** + * Move to start of buffer in input + */ + input_buffer_home?: string + /** + * Move to end of buffer in input + */ + input_buffer_end?: string + /** + * Select to start of buffer in input + */ + input_select_buffer_home?: string + /** + * Select to end of buffer in input + */ + input_select_buffer_end?: string + /** + * Delete line in input + */ + input_delete_line?: string + /** + * Delete to end of line in input + */ + input_delete_to_line_end?: string + /** + * Delete to start of line in input + */ + input_delete_to_line_start?: string + /** + * Backspace in input + */ + input_backspace?: string + /** + * Delete character in input + */ + input_delete?: string + /** + * Undo in input + */ + input_undo?: string + /** + * Redo in input + */ + input_redo?: string + /** + * Move word forward in input + */ + input_word_forward?: string + /** + * Move word backward in input + */ + input_word_backward?: string + /** + * Select word forward in input + */ + input_select_word_forward?: string + /** + * Select word backward in input + */ + input_select_word_backward?: string + /** + * Delete word forward in input + */ + input_delete_word_forward?: string + /** + * Delete word backward in input + */ + input_delete_word_backward?: string + /** + * Previous history item + */ + history_previous?: string + /** + * Next history item + */ + history_next?: string + /** + * Next child session + */ + session_child_cycle?: string + /** + * Previous child session + */ + session_child_cycle_reverse?: string + /** + * Go to parent session + */ + session_parent?: string + /** + * Suspend terminal + */ + terminal_suspend?: string + /** + * Toggle terminal title + */ + terminal_title_toggle?: string + /** + * Toggle tips on home screen + */ + tips_toggle?: string +} + +/** + * Log level + */ +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" + +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { + /** + * Port to listen on + */ + port?: number + /** + * Hostname to listen on + */ + hostname?: string + /** + * Enable mDNS service discovery + */ + mdns?: boolean + /** + * Additional domains to allow for CORS + */ + cors?: Array +} + +export type PermissionActionConfig = "ask" | "allow" | "deny" + +export type PermissionObjectConfig = { + [key: string]: PermissionActionConfig +} + +export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig + +export type PermissionConfig = + | { + __originalKeys?: Array + read?: PermissionRuleConfig + edit?: PermissionRuleConfig + glob?: PermissionRuleConfig + grep?: PermissionRuleConfig + list?: PermissionRuleConfig + bash?: PermissionRuleConfig + task?: PermissionRuleConfig + external_directory?: PermissionRuleConfig + todowrite?: PermissionActionConfig + todoread?: PermissionActionConfig + question?: PermissionActionConfig + webfetch?: PermissionActionConfig + websearch?: PermissionActionConfig + codesearch?: PermissionActionConfig + lsp?: PermissionRuleConfig + doom_loop?: PermissionActionConfig + [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined + } + | PermissionActionConfig + +export type AgentConfig = { + model?: string + temperature?: number + top_p?: number + prompt?: string + /** + * @deprecated Use 'permission' field instead */ - input_clear?: string + tools?: { + [key: string]: boolean + } + disable?: boolean /** - * Paste from clipboard + * Description of when to use the agent */ - input_paste?: string + description?: string + mode?: "subagent" | "primary" | "all" /** - * Submit input + * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) */ - input_submit?: string + hidden?: boolean + options?: { + [key: string]: unknown + } /** - * Insert newline in input + * Hex color code for the agent (e.g., #FF5733) */ - input_newline?: string + color?: string /** - * Move cursor left in input + * Maximum number of agentic iterations before forcing text-only response */ - input_move_left?: string + steps?: number /** - * Move cursor right in input + * @deprecated Use 'steps' field instead. */ - input_move_right?: string + maxSteps?: number + permission?: PermissionConfig + [key: string]: + | unknown + | string + | number + | { + [key: string]: boolean + } + | boolean + | "subagent" + | "primary" + | "all" + | { + [key: string]: unknown + } + | string + | number + | PermissionConfig + | undefined +} + +export type ProviderConfig = { + api?: string + name?: string + env?: Array + id?: string + npm?: string + models?: { + [key: string]: { + id?: string + name?: string + family?: string + release_date?: string + attachment?: boolean + reasoning?: boolean + temperature?: boolean + tool_call?: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit?: { + context: number + input?: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + options?: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + provider?: { + npm: string + } + /** + * Variant-specific configuration + */ + variants?: { + [key: string]: { + /** + * Disable this variant for the model + */ + disabled?: boolean + [key: string]: unknown | boolean | undefined + } + } + } + } + whitelist?: Array + blacklist?: Array + options?: { + apiKey?: string + baseURL?: string + /** + * GitHub Enterprise URL for copilot authentication + */ + enterpriseUrl?: string + /** + * Enable promptCacheKey for this provider (default false) + */ + setCacheKey?: boolean + /** + * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. + */ + timeout?: number | false + [key: string]: unknown | string | boolean | number | false | undefined + } +} + +export type McpLocalConfig = { /** - * Move cursor up in input + * Type of MCP server connection */ - input_move_up?: string + type: "local" /** - * Move cursor down in input + * Command and arguments to run the MCP server */ - input_move_down?: string + command: Array /** - * Select left in input + * Environment variables to set when running the MCP server */ - input_select_left?: string + environment?: { + [key: string]: string + } /** - * Select right in input + * Enable or disable the MCP server on startup */ - input_select_right?: string + enabled?: boolean /** - * Select up in input + * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. */ - input_select_up?: string + timeout?: number +} + +export type McpOAuthConfig = { /** - * Select down in input + * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. */ - input_select_down?: string + clientId?: string /** - * Move to start of line in input + * OAuth client secret (if required by the authorization server) */ - input_line_home?: string + clientSecret?: string /** - * Move to end of line in input + * OAuth scopes to request during authorization */ - input_line_end?: string + scope?: string +} + +export type McpRemoteConfig = { /** - * Select to start of line in input + * Type of MCP server connection */ - input_select_line_home?: string + type: "remote" /** - * Select to end of line in input + * URL of the remote MCP server */ - input_select_line_end?: string + url: string /** - * Move to start of visual line in input + * Enable or disable the MCP server on startup */ - input_visual_line_home?: string + enabled?: boolean /** - * Move to end of visual line in input + * Headers to send with the request */ - input_visual_line_end?: string + headers?: { + [key: string]: string + } /** - * Select to start of visual line in input + * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. */ - input_select_visual_line_home?: string + oauth?: McpOAuthConfig | false /** - * Select to end of visual line in input + * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. */ - input_select_visual_line_end?: string + timeout?: number +} + +/** + * @deprecated Always uses stretch layout. + */ +export type LayoutConfig = "auto" | "stretch" + +export type Config = { /** - * Move to start of buffer in input + * JSON schema reference for configuration validation */ - input_buffer_home?: string + $schema?: string /** - * Move to end of buffer in input + * Theme name to use for the interface */ - input_buffer_end?: string + theme?: string + keybinds?: KeybindsConfig + logLevel?: LogLevel /** - * Select to start of buffer in input + * TUI specific settings */ - input_select_buffer_home?: string + tui?: { + /** + * TUI scroll speed + */ + scroll_speed?: number + /** + * Scroll acceleration settings + */ + scroll_acceleration?: { + /** + * Enable scroll acceleration + */ + enabled: boolean + } + /** + * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column + */ + diff_style?: "auto" | "stacked" + } + server?: ServerConfig /** - * Select to end of buffer in input + * Command configuration, see https://opencode.ai/docs/commands */ - input_select_buffer_end?: string + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + subtask?: boolean + } + } + watcher?: { + ignore?: Array + } + plugin?: Array + snapshot?: boolean /** - * Delete line in input + * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing */ - input_delete_line?: string + share?: "manual" | "auto" | "disabled" /** - * Delete to end of line in input + * @deprecated Use 'share' field instead. Share newly created sessions automatically */ - input_delete_to_line_end?: string + autoshare?: boolean /** - * Delete to start of line in input + * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications */ - input_delete_to_line_start?: string + autoupdate?: boolean | "notify" /** - * Backspace in input + * Disable providers that are loaded automatically */ - input_backspace?: string + disabled_providers?: Array /** - * Delete character in input + * When set, ONLY these providers will be enabled. All other providers will be ignored */ - input_delete?: string + enabled_providers?: Array /** - * Undo in input + * Model to use in the format of provider/model, eg anthropic/claude-2 */ - input_undo?: string + model?: string /** - * Redo in input + * Small model to use for tasks like title generation in the format of provider/model */ - input_redo?: string + small_model?: string /** - * Move word forward in input + * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. */ - input_word_forward?: string + default_agent?: string /** - * Move word backward in input + * Custom username to display in conversations instead of system username */ - input_word_backward?: string + username?: string /** - * Select word forward in input + * @deprecated Use `agent` field instead. */ - input_select_word_forward?: string + mode?: { + build?: AgentConfig + plan?: AgentConfig + [key: string]: AgentConfig | undefined + } /** - * Select word backward in input + * Agent configuration, see https://opencode.ai/docs/agent */ - input_select_word_backward?: string + agent?: { + plan?: AgentConfig + build?: AgentConfig + general?: AgentConfig + explore?: AgentConfig + title?: AgentConfig + summary?: AgentConfig + compaction?: AgentConfig + [key: string]: AgentConfig | undefined + } /** - * Delete word forward in input + * Custom provider configurations and model overrides */ - input_delete_word_forward?: string + provider?: { + [key: string]: ProviderConfig + } /** - * Delete word backward in input + * MCP (Model Context Protocol) server configurations */ - input_delete_word_backward?: string + mcp?: { + [key: string]: + | McpLocalConfig + | McpRemoteConfig + | { + enabled: boolean + } + } + formatter?: + | false + | { + [key: string]: { + disabled?: boolean + command?: Array + environment?: { + [key: string]: string + } + extensions?: Array + } + } + lsp?: + | false + | { + [key: string]: + | { + disabled: true + } + | { + command: Array + extensions?: Array + disabled?: boolean + env?: { + [key: string]: string + } + initialization?: { + [key: string]: unknown + } + } + } /** - * Previous history item + * Additional instruction files or patterns to include */ - history_previous?: string + instructions?: Array + layout?: LayoutConfig + permission?: PermissionConfig + tools?: { + [key: string]: boolean + } + enterprise?: { + /** + * Enterprise URL + */ + url?: string + } + compaction?: { + /** + * Enable automatic compaction when context is full (default: true) + */ + auto?: boolean + /** + * Enable pruning of old tool outputs (default: true) + */ + prune?: boolean + } + experimental?: { + hook?: { + file_edited?: { + [key: string]: Array<{ + command: Array + environment?: { + [key: string]: string + } + }> + } + session_completed?: Array<{ + command: Array + environment?: { + [key: string]: string + } + }> + } + /** + * Number of retries for chat completions on failure + */ + chatMaxRetries?: number + disable_paste_summary?: boolean + /** + * Enable the batch tool + */ + batch_tool?: boolean + /** + * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) + */ + openTelemetry?: boolean + /** + * Tools that should only be available to primary agents. + */ + primary_tools?: Array + /** + * Continue the agent loop when a tool call is denied + */ + continue_loop_on_deny?: boolean + /** + * Timeout in milliseconds for model context protocol (MCP) requests + */ + mcp_timeout?: number + } +} + +export type EventConfigUpdated = { + type: "config.updated" + properties: Config +} + +export type EventServerInstanceDisposed = { + type: "server.instance.disposed" + properties: { + directory: string + } +} + +export type EventLspClientDiagnostics = { + type: "lsp.client.diagnostics" + properties: { + serverID: string + path: string + } +} + +export type EventLspUpdated = { + type: "lsp.updated" + properties: { + [key: string]: unknown + } +} + +export type FileDiff = { + file: string + before: string + after: string + additions: number + deletions: number +} + +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { + created: number + } + summary?: { + title?: string + body?: string + diffs: Array + } + agent: string + model: { + providerID: string + modelID: string + } + system?: string + tools?: { + [key: string]: boolean + } + variant?: string +} + +export type ProviderAuthError = { + name: "ProviderAuthError" + data: { + providerID: string + message: string + } +} + +export type UnknownError = { + name: "UnknownError" + data: { + message: string + } +} + +export type MessageOutputLengthError = { + name: "MessageOutputLengthError" + data: { + [key: string]: unknown + } +} + +export type MessageAbortedError = { + name: "MessageAbortedError" + data: { + message: string + } +} + +export type ApiError = { + name: "APIError" + data: { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string + } + responseBody?: string + metadata?: { + [key: string]: string + } + } +} + +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { + created: number + completed?: number + } + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string + } + summary?: boolean + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + finish?: string +} + +export type Message = UserMessage | AssistantMessage + +export type EventMessageUpdated = { + type: "message.updated" + properties: { + info: Message + } +} + +export type EventMessageRemoved = { + type: "message.removed" + properties: { + sessionID: string + messageID: string + } +} + +export type TextPart = { + id: string + sessionID: string + messageID: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown + } +} + +export type ReasoningPart = { + id: string + sessionID: string + messageID: string + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } +} + +export type FilePartSourceText = { + value: string + start: number + end: number +} + +export type FileSource = { + text: FilePartSourceText + type: "file" + path: string +} + +export type Range = { + start: { + line: number + character: number + } + end: { + line: number + character: number + } +} + +export type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: Range + name: string + kind: number +} + +export type ResourceSource = { + text: FilePartSourceText + type: "resource" + clientName: string + uri: string +} + +export type FilePartSource = FileSource | SymbolSource | ResourceSource + +export type FilePart = { + id: string + sessionID: string + messageID: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type ToolStatePending = { + status: "pending" + input: { + [key: string]: unknown + } + raw: string +} + +export type ToolStateRunning = { + status: "running" + input: { + [key: string]: unknown + } + title?: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + } +} + +export type ToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown + } + output: string + title: string + metadata: { + [key: string]: unknown + } + time: { + start: number + end: number + compacted?: number + } + attachments?: Array +} + +export type ToolStateError = { + status: "error" + input: { + [key: string]: unknown + } + error: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end: number + } +} + +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + +export type ToolPart = { + id: string + sessionID: string + messageID: string + type: "tool" + callID: string + tool: string + state: ToolState + metadata?: { + [key: string]: unknown + } +} + +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" + snapshot?: string +} + +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + reason: string + snapshot?: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } +} + +export type SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string +} + +export type PatchPart = { + id: string + sessionID: string + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type RetryPart = { + id: string + sessionID: string + messageID: string + type: "retry" + attempt: number + error: ApiError + time: { + created: number + } +} + +export type CompactionPart = { + id: string + sessionID: string + messageID: string + type: "compaction" + auto: boolean +} + +export type Part = + | TextPart + | { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + command?: string + } + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | RetryPart + | CompactionPart + +export type EventMessagePartUpdated = { + type: "message.part.updated" + properties: { + part: Part + delta?: string + } +} + +export type EventMessagePartRemoved = { + type: "message.part.removed" + properties: { + sessionID: string + messageID: string + partID: string + } +} + +export type PermissionRequest = { + id: string + sessionID: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown + } + always: Array + tool?: { + messageID: string + callID: string + } +} + +export type EventPermissionAsked = { + type: "permission.asked" + properties: PermissionRequest +} + +export type EventPermissionReplied = { + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" + } +} + +export type SessionStatus = + | { + type: "idle" + } + | { + type: "retry" + attempt: number + message: string + next: number + } + | { + type: "busy" + } + +export type EventSessionStatus = { + type: "session.status" + properties: { + sessionID: string + status: SessionStatus + } +} + +export type EventSessionIdle = { + type: "session.idle" + properties: { + sessionID: string + } +} + +export type QuestionOption = { /** - * Next history item + * Display text (1-5 words, concise) */ - history_next?: string + label: string /** - * Next child session + * Explanation of choice */ - session_child_cycle?: string + description: string +} + +export type QuestionInfo = { /** - * Previous child session + * Complete question */ - session_child_cycle_reverse?: string + question: string /** - * Go to parent session + * Very short label (max 12 chars) */ - session_parent?: string + header: string /** - * Suspend terminal + * Available choices */ - terminal_suspend?: string + options: Array /** - * Toggle terminal title + * Allow selecting multiple choices */ - terminal_title_toggle?: string + multiple?: boolean /** - * Toggle tips on home screen + * Allow typing a custom answer (default: true) */ - tips_toggle?: string + custom?: boolean } -/** - * Log level - */ -export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" - -/** - * Server configuration for opencode serve and web commands - */ -export type ServerConfig = { - /** - * Port to listen on - */ - port?: number - /** - * Hostname to listen on - */ - hostname?: string - /** - * Enable mDNS service discovery - */ - mdns?: boolean +export type QuestionRequest = { + id: string + sessionID: string /** - * Additional domains to allow for CORS + * Questions to ask */ - cors?: Array + questions: Array + tool?: { + messageID: string + callID: string + } } -export type PermissionActionConfig = "ask" | "allow" | "deny" +export type EventQuestionAsked = { + type: "question.asked" + properties: QuestionRequest +} -export type PermissionObjectConfig = { - [key: string]: PermissionActionConfig +export type QuestionAnswer = Array + +export type EventQuestionReplied = { + type: "question.replied" + properties: { + sessionID: string + requestID: string + answers: Array + } } -export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig +export type EventQuestionRejected = { + type: "question.rejected" + properties: { + sessionID: string + requestID: string + } +} -export type PermissionConfig = - | { - __originalKeys?: Array - read?: PermissionRuleConfig - edit?: PermissionRuleConfig - glob?: PermissionRuleConfig - grep?: PermissionRuleConfig - list?: PermissionRuleConfig - bash?: PermissionRuleConfig - task?: PermissionRuleConfig - external_directory?: PermissionRuleConfig - todowrite?: PermissionActionConfig - todoread?: PermissionActionConfig - webfetch?: PermissionActionConfig - websearch?: PermissionActionConfig - codesearch?: PermissionActionConfig - lsp?: PermissionRuleConfig - doom_loop?: PermissionActionConfig - [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined - } - | PermissionActionConfig +export type EventSessionCompacted = { + type: "session.compacted" + properties: { + sessionID: string + } +} -export type AgentConfig = { - model?: string - temperature?: number - top_p?: number - prompt?: string - /** - * @deprecated Use 'permission' field instead - */ - tools?: { - [key: string]: boolean +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string } - disable?: boolean - /** - * Description of when to use the agent - */ - description?: string - mode?: "subagent" | "primary" | "all" +} + +export type Todo = { /** - * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) + * Brief description of the task */ - hidden?: boolean - options?: { - [key: string]: unknown - } + content: string /** - * Hex color code for the agent (e.g., #FF5733) + * Current status of the task: pending, in_progress, completed, cancelled */ - color?: string + status: string /** - * Maximum number of agentic iterations before forcing text-only response + * Priority level of the task: high, medium, low */ - steps?: number + priority: string /** - * @deprecated Use 'steps' field instead. + * Unique identifier for the todo item */ - maxSteps?: number - permission?: PermissionConfig - [key: string]: - | unknown - | string - | number - | { - [key: string]: boolean - } - | boolean - | "subagent" - | "primary" - | "all" - | { - [key: string]: unknown - } - | string - | number - | PermissionConfig - | undefined + id: string } -export type ProviderConfig = { - api?: string - name?: string - env?: Array - id?: string - npm?: string - models?: { +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array + } +} + +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + +export type EventSkillUpdated = { + type: "skill.updated" + properties: { [key: string]: { - id?: string - name?: string - family?: string - release_date?: string - attachment?: boolean - reasoning?: boolean - temperature?: boolean - tool_call?: boolean - interleaved?: - | true - | { - field: "reasoning_content" | "reasoning_details" - } - cost?: { - input: number - output: number - cache_read?: number - cache_write?: number - context_over_200k?: { - input: number - output: number - cache_read?: number - cache_write?: number - } - } - limit?: { - context: number - output: number - } - modalities?: { - input: Array<"text" | "audio" | "image" | "video" | "pdf"> - output: Array<"text" | "audio" | "image" | "video" | "pdf"> - } - experimental?: boolean - status?: "alpha" | "beta" | "deprecated" - options?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - provider?: { - npm: string - } - /** - * Variant-specific configuration - */ - variants?: { - [key: string]: { - /** - * Disable this variant for the model - */ - disabled?: boolean - [key: string]: unknown | boolean | undefined - } - } + name: string + description: string + location: string } } - whitelist?: Array - blacklist?: Array - options?: { - apiKey?: string - baseURL?: string - /** - * GitHub Enterprise URL for copilot authentication - */ - enterpriseUrl?: string - /** - * Enable promptCacheKey for this provider (default false) - */ - setCacheKey?: boolean - /** - * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. - */ - timeout?: number | false - [key: string]: unknown | string | boolean | number | false | undefined +} + +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } +} + +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } +} + +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number + } +} + +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string + } +} + +export type EventMcpToolsChanged = { + type: "mcp.tools.changed" + properties: { + server: string + } +} + +export type EventCommandUpdated = { + type: "command.updated" + properties: { + [key: string]: unknown + } +} + +export type EventCommandExecuted = { + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } +} + +export type PermissionAction = "allow" | "deny" | "ask" + +export type PermissionRule = { + permission: string + pattern: string + action: PermissionAction +} + +export type PermissionRuleset = Array + +export type Session = { + id: string + slug: string + projectID: string + directory: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array + } + share?: { + url: string + } + title: string + version: string + time: { + created: number + updated: number + compacting?: number + archived?: number + } + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string } } -export type McpLocalConfig = { - /** - * Type of MCP server connection - */ - type: "local" - /** - * Command and arguments to run the MCP server - */ - command: Array - /** - * Environment variables to set when running the MCP server - */ - environment?: { - [key: string]: string +export type EventSessionCreated = { + type: "session.created" + properties: { + info: Session } - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number } -export type McpOAuthConfig = { - /** - * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. - */ - clientId?: string - /** - * OAuth client secret (if required by the authorization server) - */ - clientSecret?: string - /** - * OAuth scopes to request during authorization - */ - scope?: string +export type EventSessionUpdated = { + type: "session.updated" + properties: { + info: Session + } } -export type McpRemoteConfig = { - /** - * Type of MCP server connection - */ - type: "remote" - /** - * URL of the remote MCP server - */ - url: string - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Headers to send with the request - */ - headers?: { - [key: string]: string +export type EventSessionDeleted = { + type: "session.deleted" + properties: { + info: Session } - /** - * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. - */ - oauth?: McpOAuthConfig | false - /** - * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number } -/** - * @deprecated Always uses stretch layout. - */ -export type LayoutConfig = "auto" | "stretch" - -export type Config = { - /** - * JSON schema reference for configuration validation - */ - $schema?: string - /** - * Theme name to use for the interface - */ - theme?: string - keybinds?: KeybindsConfig - logLevel?: LogLevel - /** - * TUI specific settings - */ - tui?: { - /** - * TUI scroll speed - */ - scroll_speed?: number - /** - * Scroll acceleration settings - */ - scroll_acceleration?: { - /** - * Enable scroll acceleration - */ - enabled: boolean - } - /** - * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column - */ - diff_style?: "auto" | "stacked" - } - server?: ServerConfig - /** - * Command configuration, see https://opencode.ai/docs/commands - */ - command?: { - [key: string]: { - template: string - description?: string - agent?: string - model?: string - subtask?: boolean - } - } - watcher?: { - ignore?: Array - } - plugin?: Array - snapshot?: boolean - /** - * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing - */ - share?: "manual" | "auto" | "disabled" - /** - * @deprecated Use 'share' field instead. Share newly created sessions automatically - */ - autoshare?: boolean - /** - * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications - */ - autoupdate?: boolean | "notify" - /** - * Disable providers that are loaded automatically - */ - disabled_providers?: Array - /** - * When set, ONLY these providers will be enabled. All other providers will be ignored - */ - enabled_providers?: Array - /** - * Model to use in the format of provider/model, eg anthropic/claude-2 - */ - model?: string - /** - * Small model to use for tasks like title generation in the format of provider/model - */ - small_model?: string - /** - * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. - */ - default_agent?: string - /** - * Custom username to display in conversations instead of system username - */ - username?: string - /** - * @deprecated Use `agent` field instead. - */ - mode?: { - build?: AgentConfig - plan?: AgentConfig - [key: string]: AgentConfig | undefined +export type EventSessionDiff = { + type: "session.diff" + properties: { + sessionID: string + diff: Array } - /** - * Agent configuration, see https://opencode.ai/docs/agent - */ - agent?: { - plan?: AgentConfig - build?: AgentConfig - general?: AgentConfig - explore?: AgentConfig - title?: AgentConfig - summary?: AgentConfig - compaction?: AgentConfig - [key: string]: AgentConfig | undefined +} + +export type EventSessionError = { + type: "session.error" + properties: { + sessionID?: string + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError } - /** - * Custom provider configurations and model overrides - */ - provider?: { - [key: string]: ProviderConfig +} + +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" + properties: { + branch?: string } - /** - * MCP (Model Context Protocol) server configurations - */ - mcp?: { - [key: string]: - | McpLocalConfig - | McpRemoteConfig - | { - enabled: boolean - } +} + +export type Pty = { + id: string + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number +} + +export type EventPtyCreated = { + type: "pty.created" + properties: { + info: Pty } - formatter?: - | false - | { - [key: string]: { - disabled?: boolean - command?: Array - environment?: { - [key: string]: string - } - extensions?: Array - } - } - lsp?: - | false - | { - [key: string]: - | { - disabled: true - } - | { - command: Array - extensions?: Array - disabled?: boolean - env?: { - [key: string]: string - } - initialization?: { - [key: string]: unknown - } - } - } - /** - * Additional instruction files or patterns to include - */ - instructions?: Array - layout?: LayoutConfig - permission?: PermissionConfig - tools?: { - [key: string]: boolean +} + +export type EventPtyUpdated = { + type: "pty.updated" + properties: { + info: Pty } - enterprise?: { - /** - * Enterprise URL - */ - url?: string +} + +export type EventPtyExited = { + type: "pty.exited" + properties: { + id: string + exitCode: number } - compaction?: { - /** - * Enable automatic compaction when context is full (default: true) - */ - auto?: boolean - /** - * Enable pruning of old tool outputs (default: true) - */ - prune?: boolean +} + +export type EventPtyDeleted = { + type: "pty.deleted" + properties: { + id: string } - experimental?: { - hook?: { - file_edited?: { - [key: string]: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> - } - session_completed?: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> - } - /** - * Number of retries for chat completions on failure - */ - chatMaxRetries?: number - disable_paste_summary?: boolean - /** - * Enable the batch tool - */ - batch_tool?: boolean - /** - * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) - */ - openTelemetry?: boolean - /** - * Tools that should only be available to primary agents. - */ - primary_tools?: Array - /** - * Continue the agent loop when a tool call is denied - */ - continue_loop_on_deny?: boolean - /** - * Timeout in milliseconds for model context protocol (MCP) requests - */ - mcp_timeout?: number +} + +export type EventServerConnected = { + type: "server.connected" + properties: { + [key: string]: unknown + } +} + +export type EventGlobalDisposed = { + type: "global.disposed" + properties: { + [key: string]: unknown + } +} + +export type Event = + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventProjectUpdated + | EventConfigUpdated + | EventServerInstanceDisposed + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventPermissionAsked + | EventPermissionReplied + | EventSessionStatus + | EventSessionIdle + | EventQuestionAsked + | EventQuestionReplied + | EventQuestionRejected + | EventSessionCompacted + | EventFileEdited + | EventTodoUpdated + | EventFileWatcherUpdated + | EventSkillUpdated + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventCommandUpdated + | EventCommandExecuted + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionDiff + | EventSessionError + | EventVcsBranchUpdated + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventServerConnected + | EventGlobalDisposed + +export type GlobalEvent = { + directory: string + payload: Event +} + +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string } } @@ -1827,6 +1946,7 @@ export type Model = { } limit: { context: number + input?: number output: number } status: "alpha" | "beta" | "deprecated" | "active" @@ -1991,6 +2111,7 @@ export type OAuth = { refresh: string access: string expires: number + accountId?: string enterpriseUrl?: string } @@ -2512,7 +2633,14 @@ export type SessionListData = { body?: never path?: never query?: { + /** + * Filter sessions by project directory + */ directory?: string + /** + * Only return root sessions (no parentID) + */ + roots?: boolean /** * Filter sessions updated on or after this timestamp (milliseconds since epoch) */ @@ -3545,6 +3673,95 @@ export type PermissionListResponses = { export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] +export type QuestionListData = { + body?: never + path?: never + query?: { + directory?: string + } + url: "/question" +} + +export type QuestionListResponses = { + /** + * List of pending questions + */ + 200: Array +} + +export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] + +export type QuestionReplyData = { + body?: { + /** + * User answers in order of questions (each answer is an array of selected labels) + */ + answers: Array + } + path: { + requestID: string + } + query?: { + directory?: string + } + url: "/question/{requestID}/reply" +} + +export type QuestionReplyErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors] + +export type QuestionReplyResponses = { + /** + * Question answered successfully + */ + 200: boolean +} + +export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] + +export type QuestionRejectData = { + body?: never + path: { + requestID: string + } + query?: { + directory?: string + } + url: "/question/{requestID}/reject" +} + +export type QuestionRejectErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors] + +export type QuestionRejectResponses = { + /** + * Question rejected successfully + */ + 200: boolean +} + +export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses] + export type CommandListData = { body?: never path?: never @@ -3635,6 +3852,7 @@ export type ProviderListResponses = { } limit: { context: number + input?: number output: number } modalities?: { From 97ca26edbe909d44dad5a68e002fc408e05eda7a Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:42:23 +0000 Subject: [PATCH 26/27] Revert "chore(sdk): regenerate api" This reverts commit 84e47d1708d534fb1d66ddd32e10a886bfc09587. --- packages/sdk/js/src/v2/gen/types.gen.ts | 40 +++++++++++-------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index d198027633b..cbace013033 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -40,6 +40,21 @@ export type EventProjectUpdated = { properties: Project } +export type EventServerInstanceDisposed = { + type: "server.instance.disposed" + properties: { + directory: string + } +} + +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" + } +} + /** * Custom keybind configurations */ @@ -918,13 +933,6 @@ export type EventConfigUpdated = { properties: Config } -export type EventServerInstanceDisposed = { - type: "server.instance.disposed" - properties: { - directory: string - } -} - export type EventLspClientDiagnostics = { type: "lsp.client.diagnostics" properties: { @@ -1507,22 +1515,10 @@ export type EventTodoUpdated = { } } -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" - properties: { - file: string - event: "add" | "change" | "unlink" - } -} - export type EventSkillUpdated = { type: "skill.updated" properties: { - [key: string]: { - name: string - description: string - location: string - } + [key: string]: unknown } } @@ -1745,8 +1741,9 @@ export type Event = | EventInstallationUpdated | EventInstallationUpdateAvailable | EventProjectUpdated - | EventConfigUpdated | EventServerInstanceDisposed + | EventFileWatcherUpdated + | EventConfigUpdated | EventLspClientDiagnostics | EventLspUpdated | EventMessageUpdated @@ -1763,7 +1760,6 @@ export type Event = | EventSessionCompacted | EventFileEdited | EventTodoUpdated - | EventFileWatcherUpdated | EventSkillUpdated | EventTuiPromptAppend | EventTuiCommandExecute From 16a9d11975478a987360bda4226a541dff63d27f Mon Sep 17 00:00:00 2001 From: OpenCode Agent Date: Thu, 15 Jan 2026 19:44:44 +0000 Subject: [PATCH 27/27] chore(sdk): align generated sdk with dev --- packages/sdk/js/src/v2/gen/sdk.gen.ts | 98 - packages/sdk/js/src/v2/gen/types.gen.ts | 3052 +++++++++++------------ 2 files changed, 1419 insertions(+), 1731 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 697dac7eefe..a26cefb176f 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -84,12 +84,6 @@ import type { PtyRemoveResponses, PtyUpdateErrors, PtyUpdateResponses, - QuestionAnswer, - QuestionListResponses, - QuestionRejectErrors, - QuestionRejectResponses, - QuestionReplyErrors, - QuestionReplyResponses, SessionAbortErrors, SessionAbortResponses, SessionChildrenErrors, @@ -781,7 +775,6 @@ export class Session extends HeyApiClient { public list( parameters?: { directory?: string - roots?: boolean start?: number search?: string limit?: number @@ -794,7 +787,6 @@ export class Session extends HeyApiClient { { args: [ { in: "query", key: "directory" }, - { in: "query", key: "roots" }, { in: "query", key: "start" }, { in: "query", key: "search" }, { in: "query", key: "limit" }, @@ -1789,94 +1781,6 @@ export class Permission extends HeyApiClient { } } -export class Question extends HeyApiClient { - /** - * List pending questions - * - * Get all pending question requests across all sessions. - */ - public list( - parameters?: { - directory?: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }] }]) - return (options?.client ?? this.client).get({ - url: "/question", - ...options, - ...params, - }) - } - - /** - * Reply to question request - * - * Provide answers to a question request from the AI assistant. - */ - public reply( - parameters: { - requestID: string - directory?: string - answers?: Array - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "requestID" }, - { in: "query", key: "directory" }, - { in: "body", key: "answers" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/question/{requestID}/reply", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Reject question request - * - * Reject a question request from the AI assistant. - */ - public reject( - parameters: { - requestID: string - directory?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "requestID" }, - { in: "query", key: "directory" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/question/{requestID}/reject", - ...options, - ...params, - }) - } -} - export class Command extends HeyApiClient { /** * List commands @@ -3008,8 +2912,6 @@ export class OpencodeClient extends HeyApiClient { permission = new Permission({ client: this.client }) - question = new Question({ client: this.client }) - command = new Command({ client: this.client }) provider = new Provider({ client: this.client }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index cbace013033..97a695162ed 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -47,1757 +47,1642 @@ export type EventServerInstanceDisposed = { } } -export type EventFileWatcherUpdated = { - type: "file.watcher.updated" +export type EventLspClientDiagnostics = { + type: "lsp.client.diagnostics" properties: { - file: string - event: "add" | "change" | "unlink" + serverID: string + path: string } } -/** - * Custom keybind configurations - */ -export type KeybindsConfig = { - /** - * Leader key for keybind combinations - */ - leader?: string - /** - * Exit the application - */ - app_exit?: string - /** - * Open external editor - */ - editor_open?: string - /** - * List available themes - */ - theme_list?: string - /** - * Toggle sidebar - */ - sidebar_toggle?: string - /** - * Toggle session scrollbar - */ - scrollbar_toggle?: string - /** - * Toggle username visibility - */ - username_toggle?: string - /** - * View status - */ - status_view?: string - /** - * Export session to editor - */ - session_export?: string - /** - * Create a new session - */ - session_new?: string - /** - * List all sessions - */ - session_list?: string - /** - * Show session timeline - */ - session_timeline?: string - /** - * Fork session from message - */ - session_fork?: string - /** - * Rename session - */ - session_rename?: string - /** - * Delete session - */ - session_delete?: string - /** - * Delete stash entry - */ - stash_delete?: string - /** - * Open provider list from model dialog - */ - model_provider_list?: string - /** - * Toggle model favorite status - */ - model_favorite_toggle?: string - /** - * Share current session - */ - session_share?: string - /** - * Unshare current session - */ - session_unshare?: string - /** - * Interrupt current session - */ - session_interrupt?: string - /** - * Compact the session - */ - session_compact?: string - /** - * Scroll messages up by one page - */ - messages_page_up?: string - /** - * Scroll messages down by one page - */ - messages_page_down?: string - /** - * Scroll messages up by half page - */ - messages_half_page_up?: string - /** - * Scroll messages down by half page - */ - messages_half_page_down?: string - /** - * Navigate to first message - */ - messages_first?: string - /** - * Navigate to last message - */ - messages_last?: string - /** - * Navigate to next message - */ - messages_next?: string - /** - * Navigate to previous message - */ - messages_previous?: string - /** - * Navigate to last user message - */ - messages_last_user?: string - /** - * Copy message - */ - messages_copy?: string - /** - * Undo message - */ - messages_undo?: string - /** - * Redo message - */ - messages_redo?: string - /** - * Toggle code block concealment in messages - */ - messages_toggle_conceal?: string - /** - * Toggle tool details visibility - */ - tool_details?: string - /** - * List available models - */ - model_list?: string - /** - * Next recently used model - */ - model_cycle_recent?: string - /** - * Previous recently used model - */ - model_cycle_recent_reverse?: string - /** - * Next favorite model - */ - model_cycle_favorite?: string - /** - * Previous favorite model - */ - model_cycle_favorite_reverse?: string - /** - * List available commands - */ - command_list?: string - /** - * List agents - */ - agent_list?: string - /** - * Next agent - */ - agent_cycle?: string - /** - * Previous agent - */ - agent_cycle_reverse?: string - /** - * Cycle model variants - */ - variant_cycle?: string - /** - * Clear input field - */ - input_clear?: string - /** - * Paste from clipboard - */ - input_paste?: string - /** - * Submit input - */ - input_submit?: string - /** - * Insert newline in input - */ - input_newline?: string - /** - * Move cursor left in input - */ - input_move_left?: string - /** - * Move cursor right in input - */ - input_move_right?: string - /** - * Move cursor up in input - */ - input_move_up?: string - /** - * Move cursor down in input - */ - input_move_down?: string - /** - * Select left in input - */ - input_select_left?: string - /** - * Select right in input - */ - input_select_right?: string - /** - * Select up in input - */ - input_select_up?: string - /** - * Select down in input - */ - input_select_down?: string - /** - * Move to start of line in input - */ - input_line_home?: string - /** - * Move to end of line in input - */ - input_line_end?: string - /** - * Select to start of line in input - */ - input_select_line_home?: string - /** - * Select to end of line in input - */ - input_select_line_end?: string - /** - * Move to start of visual line in input - */ - input_visual_line_home?: string - /** - * Move to end of visual line in input - */ - input_visual_line_end?: string - /** - * Select to start of visual line in input - */ - input_select_visual_line_home?: string - /** - * Select to end of visual line in input - */ - input_select_visual_line_end?: string - /** - * Move to start of buffer in input - */ - input_buffer_home?: string - /** - * Move to end of buffer in input - */ - input_buffer_end?: string - /** - * Select to start of buffer in input - */ - input_select_buffer_home?: string - /** - * Select to end of buffer in input - */ - input_select_buffer_end?: string - /** - * Delete line in input - */ - input_delete_line?: string - /** - * Delete to end of line in input - */ - input_delete_to_line_end?: string - /** - * Delete to start of line in input - */ - input_delete_to_line_start?: string - /** - * Backspace in input - */ - input_backspace?: string - /** - * Delete character in input - */ - input_delete?: string - /** - * Undo in input - */ - input_undo?: string - /** - * Redo in input - */ - input_redo?: string - /** - * Move word forward in input - */ - input_word_forward?: string - /** - * Move word backward in input - */ - input_word_backward?: string - /** - * Select word forward in input - */ - input_select_word_forward?: string - /** - * Select word backward in input - */ - input_select_word_backward?: string - /** - * Delete word forward in input - */ - input_delete_word_forward?: string - /** - * Delete word backward in input - */ - input_delete_word_backward?: string - /** - * Previous history item - */ - history_previous?: string - /** - * Next history item - */ - history_next?: string - /** - * Next child session - */ - session_child_cycle?: string - /** - * Previous child session - */ - session_child_cycle_reverse?: string - /** - * Go to parent session - */ - session_parent?: string - /** - * Suspend terminal - */ - terminal_suspend?: string - /** - * Toggle terminal title - */ - terminal_title_toggle?: string - /** - * Toggle tips on home screen - */ - tips_toggle?: string +export type EventLspUpdated = { + type: "lsp.updated" + properties: { + [key: string]: unknown + } } -/** - * Log level - */ -export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" +export type FileDiff = { + file: string + before: string + after: string + additions: number + deletions: number +} -/** - * Server configuration for opencode serve and web commands - */ -export type ServerConfig = { - /** - * Port to listen on - */ - port?: number - /** - * Hostname to listen on - */ - hostname?: string - /** - * Enable mDNS service discovery - */ - mdns?: boolean - /** - * Additional domains to allow for CORS - */ - cors?: Array +export type UserMessage = { + id: string + sessionID: string + role: "user" + time: { + created: number + } + summary?: { + title?: string + body?: string + diffs: Array + } + agent: string + model: { + providerID: string + modelID: string + } + system?: string + tools?: { + [key: string]: boolean + } + variant?: string } -export type PermissionActionConfig = "ask" | "allow" | "deny" +export type ProviderAuthError = { + name: "ProviderAuthError" + data: { + providerID: string + message: string + } +} -export type PermissionObjectConfig = { - [key: string]: PermissionActionConfig +export type UnknownError = { + name: "UnknownError" + data: { + message: string + } } -export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig +export type MessageOutputLengthError = { + name: "MessageOutputLengthError" + data: { + [key: string]: unknown + } +} -export type PermissionConfig = - | { - __originalKeys?: Array - read?: PermissionRuleConfig - edit?: PermissionRuleConfig - glob?: PermissionRuleConfig - grep?: PermissionRuleConfig - list?: PermissionRuleConfig - bash?: PermissionRuleConfig - task?: PermissionRuleConfig - external_directory?: PermissionRuleConfig - todowrite?: PermissionActionConfig - todoread?: PermissionActionConfig - question?: PermissionActionConfig - webfetch?: PermissionActionConfig - websearch?: PermissionActionConfig - codesearch?: PermissionActionConfig - lsp?: PermissionRuleConfig - doom_loop?: PermissionActionConfig - [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined +export type MessageAbortedError = { + name: "MessageAbortedError" + data: { + message: string + } +} + +export type ApiError = { + name: "APIError" + data: { + message: string + statusCode?: number + isRetryable: boolean + responseHeaders?: { + [key: string]: string } - | PermissionActionConfig + responseBody?: string + metadata?: { + [key: string]: string + } + } +} -export type AgentConfig = { - model?: string - temperature?: number - top_p?: number - prompt?: string - /** - * @deprecated Use 'permission' field instead - */ - tools?: { - [key: string]: boolean +export type AssistantMessage = { + id: string + sessionID: string + role: "assistant" + time: { + created: number + completed?: number } - disable?: boolean - /** - * Description of when to use the agent - */ - description?: string - mode?: "subagent" | "primary" | "all" - /** - * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) - */ - hidden?: boolean - options?: { - [key: string]: unknown + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError + parentID: string + modelID: string + providerID: string + mode: string + agent: string + path: { + cwd: string + root: string } - /** - * Hex color code for the agent (e.g., #FF5733) - */ - color?: string - /** - * Maximum number of agentic iterations before forcing text-only response - */ - steps?: number - /** - * @deprecated Use 'steps' field instead. - */ - maxSteps?: number - permission?: PermissionConfig - [key: string]: - | unknown - | string - | number - | { - [key: string]: boolean - } - | boolean - | "subagent" - | "primary" - | "all" - | { - [key: string]: unknown - } - | string - | number - | PermissionConfig - | undefined + summary?: boolean + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number + } + } + finish?: string } -export type ProviderConfig = { - api?: string - name?: string - env?: Array - id?: string - npm?: string - models?: { - [key: string]: { - id?: string - name?: string - family?: string - release_date?: string - attachment?: boolean - reasoning?: boolean - temperature?: boolean - tool_call?: boolean - interleaved?: - | true - | { - field: "reasoning_content" | "reasoning_details" - } - cost?: { - input: number - output: number - cache_read?: number - cache_write?: number - context_over_200k?: { - input: number - output: number - cache_read?: number - cache_write?: number - } - } - limit?: { - context: number - input?: number - output: number - } - modalities?: { - input: Array<"text" | "audio" | "image" | "video" | "pdf"> - output: Array<"text" | "audio" | "image" | "video" | "pdf"> - } - experimental?: boolean - status?: "alpha" | "beta" | "deprecated" - options?: { - [key: string]: unknown - } - headers?: { - [key: string]: string - } - provider?: { - npm: string - } - /** - * Variant-specific configuration - */ - variants?: { - [key: string]: { - /** - * Disable this variant for the model - */ - disabled?: boolean - [key: string]: unknown | boolean | undefined - } - } - } +export type Message = UserMessage | AssistantMessage + +export type EventMessageUpdated = { + type: "message.updated" + properties: { + info: Message } - whitelist?: Array - blacklist?: Array - options?: { - apiKey?: string - baseURL?: string - /** - * GitHub Enterprise URL for copilot authentication - */ - enterpriseUrl?: string - /** - * Enable promptCacheKey for this provider (default false) - */ - setCacheKey?: boolean - /** - * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. - */ - timeout?: number | false - [key: string]: unknown | string | boolean | number | false | undefined +} + +export type EventMessageRemoved = { + type: "message.removed" + properties: { + sessionID: string + messageID: string } } -export type McpLocalConfig = { - /** - * Type of MCP server connection - */ - type: "local" - /** - * Command and arguments to run the MCP server - */ - command: Array - /** - * Environment variables to set when running the MCP server - */ - environment?: { - [key: string]: string +export type TextPart = { + id: string + sessionID: string + messageID: string + type: "text" + text: string + synthetic?: boolean + ignored?: boolean + time?: { + start: number + end?: number + } + metadata?: { + [key: string]: unknown } - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number } -export type McpOAuthConfig = { - /** - * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. - */ - clientId?: string - /** - * OAuth client secret (if required by the authorization server) - */ - clientSecret?: string - /** - * OAuth scopes to request during authorization - */ - scope?: string +export type ReasoningPart = { + id: string + sessionID: string + messageID: string + type: "reasoning" + text: string + metadata?: { + [key: string]: unknown + } + time: { + start: number + end?: number + } } -export type McpRemoteConfig = { - /** - * Type of MCP server connection - */ - type: "remote" - /** - * URL of the remote MCP server - */ - url: string - /** - * Enable or disable the MCP server on startup - */ - enabled?: boolean - /** - * Headers to send with the request - */ - headers?: { - [key: string]: string - } - /** - * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. - */ - oauth?: McpOAuthConfig | false - /** - * Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified. - */ - timeout?: number +export type FilePartSourceText = { + value: string + start: number + end: number } -/** - * @deprecated Always uses stretch layout. - */ -export type LayoutConfig = "auto" | "stretch" +export type FileSource = { + text: FilePartSourceText + type: "file" + path: string +} -export type Config = { - /** - * JSON schema reference for configuration validation - */ - $schema?: string - /** - * Theme name to use for the interface - */ - theme?: string - keybinds?: KeybindsConfig - logLevel?: LogLevel - /** - * TUI specific settings - */ - tui?: { - /** - * TUI scroll speed - */ - scroll_speed?: number - /** - * Scroll acceleration settings - */ - scroll_acceleration?: { - /** - * Enable scroll acceleration - */ - enabled: boolean - } - /** - * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column - */ - diff_style?: "auto" | "stacked" +export type Range = { + start: { + line: number + character: number } - server?: ServerConfig - /** - * Command configuration, see https://opencode.ai/docs/commands - */ - command?: { - [key: string]: { - template: string - description?: string - agent?: string - model?: string - subtask?: boolean - } + end: { + line: number + character: number } - watcher?: { - ignore?: Array +} + +export type SymbolSource = { + text: FilePartSourceText + type: "symbol" + path: string + range: Range + name: string + kind: number +} + +export type ResourceSource = { + text: FilePartSourceText + type: "resource" + clientName: string + uri: string +} + +export type FilePartSource = FileSource | SymbolSource | ResourceSource + +export type FilePart = { + id: string + sessionID: string + messageID: string + type: "file" + mime: string + filename?: string + url: string + source?: FilePartSource +} + +export type ToolStatePending = { + status: "pending" + input: { + [key: string]: unknown } - plugin?: Array - snapshot?: boolean - /** - * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing - */ - share?: "manual" | "auto" | "disabled" - /** - * @deprecated Use 'share' field instead. Share newly created sessions automatically - */ - autoshare?: boolean - /** - * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications - */ - autoupdate?: boolean | "notify" - /** - * Disable providers that are loaded automatically - */ - disabled_providers?: Array - /** - * When set, ONLY these providers will be enabled. All other providers will be ignored - */ - enabled_providers?: Array - /** - * Model to use in the format of provider/model, eg anthropic/claude-2 - */ - model?: string - /** - * Small model to use for tasks like title generation in the format of provider/model - */ - small_model?: string - /** - * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. - */ - default_agent?: string - /** - * Custom username to display in conversations instead of system username - */ - username?: string - /** - * @deprecated Use `agent` field instead. - */ - mode?: { - build?: AgentConfig - plan?: AgentConfig - [key: string]: AgentConfig | undefined + raw: string +} + +export type ToolStateRunning = { + status: "running" + input: { + [key: string]: unknown } - /** - * Agent configuration, see https://opencode.ai/docs/agent - */ - agent?: { - plan?: AgentConfig - build?: AgentConfig - general?: AgentConfig - explore?: AgentConfig - title?: AgentConfig - summary?: AgentConfig - compaction?: AgentConfig - [key: string]: AgentConfig | undefined + title?: string + metadata?: { + [key: string]: unknown + } + time: { + start: number } - /** - * Custom provider configurations and model overrides - */ - provider?: { - [key: string]: ProviderConfig +} + +export type ToolStateCompleted = { + status: "completed" + input: { + [key: string]: unknown } - /** - * MCP (Model Context Protocol) server configurations - */ - mcp?: { - [key: string]: - | McpLocalConfig - | McpRemoteConfig - | { - enabled: boolean - } + output: string + title: string + metadata: { + [key: string]: unknown } - formatter?: - | false - | { - [key: string]: { - disabled?: boolean - command?: Array - environment?: { - [key: string]: string - } - extensions?: Array - } - } - lsp?: - | false - | { - [key: string]: - | { - disabled: true - } - | { - command: Array - extensions?: Array - disabled?: boolean - env?: { - [key: string]: string - } - initialization?: { - [key: string]: unknown - } - } - } - /** - * Additional instruction files or patterns to include - */ - instructions?: Array - layout?: LayoutConfig - permission?: PermissionConfig - tools?: { - [key: string]: boolean + time: { + start: number + end: number + compacted?: number } - enterprise?: { - /** - * Enterprise URL - */ - url?: string + attachments?: Array +} + +export type ToolStateError = { + status: "error" + input: { + [key: string]: unknown } - compaction?: { - /** - * Enable automatic compaction when context is full (default: true) - */ - auto?: boolean - /** - * Enable pruning of old tool outputs (default: true) - */ - prune?: boolean + error: string + metadata?: { + [key: string]: unknown } - experimental?: { - hook?: { - file_edited?: { - [key: string]: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> - } - session_completed?: Array<{ - command: Array - environment?: { - [key: string]: string - } - }> + time: { + start: number + end: number + } +} + +export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError + +export type ToolPart = { + id: string + sessionID: string + messageID: string + type: "tool" + callID: string + tool: string + state: ToolState + metadata?: { + [key: string]: unknown + } +} + +export type StepStartPart = { + id: string + sessionID: string + messageID: string + type: "step-start" + snapshot?: string +} + +export type StepFinishPart = { + id: string + sessionID: string + messageID: string + type: "step-finish" + reason: string + snapshot?: string + cost: number + tokens: { + input: number + output: number + reasoning: number + cache: { + read: number + write: number } - /** - * Number of retries for chat completions on failure - */ - chatMaxRetries?: number - disable_paste_summary?: boolean - /** - * Enable the batch tool - */ - batch_tool?: boolean - /** - * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) - */ - openTelemetry?: boolean - /** - * Tools that should only be available to primary agents. - */ - primary_tools?: Array - /** - * Continue the agent loop when a tool call is denied - */ - continue_loop_on_deny?: boolean - /** - * Timeout in milliseconds for model context protocol (MCP) requests - */ - mcp_timeout?: number } } -export type EventConfigUpdated = { - type: "config.updated" - properties: Config +export type SnapshotPart = { + id: string + sessionID: string + messageID: string + type: "snapshot" + snapshot: string } -export type EventLspClientDiagnostics = { - type: "lsp.client.diagnostics" +export type PatchPart = { + id: string + sessionID: string + messageID: string + type: "patch" + hash: string + files: Array +} + +export type AgentPart = { + id: string + sessionID: string + messageID: string + type: "agent" + name: string + source?: { + value: string + start: number + end: number + } +} + +export type RetryPart = { + id: string + sessionID: string + messageID: string + type: "retry" + attempt: number + error: ApiError + time: { + created: number + } +} + +export type CompactionPart = { + id: string + sessionID: string + messageID: string + type: "compaction" + auto: boolean +} + +export type Part = + | TextPart + | { + id: string + sessionID: string + messageID: string + type: "subtask" + prompt: string + description: string + agent: string + command?: string + } + | ReasoningPart + | FilePart + | ToolPart + | StepStartPart + | StepFinishPart + | SnapshotPart + | PatchPart + | AgentPart + | RetryPart + | CompactionPart + +export type EventMessagePartUpdated = { + type: "message.part.updated" properties: { - serverID: string - path: string + part: Part + delta?: string } } -export type EventLspUpdated = { - type: "lsp.updated" +export type EventMessagePartRemoved = { + type: "message.part.removed" properties: { - [key: string]: unknown + sessionID: string + messageID: string + partID: string } } -export type FileDiff = { - file: string - before: string - after: string - additions: number - deletions: number -} - -export type UserMessage = { +export type PermissionRequest = { id: string sessionID: string - role: "user" - time: { - created: number - } - summary?: { - title?: string - body?: string - diffs: Array - } - agent: string - model: { - providerID: string - modelID: string - } - system?: string - tools?: { - [key: string]: boolean - } - variant?: string -} - -export type ProviderAuthError = { - name: "ProviderAuthError" - data: { - providerID: string - message: string + permission: string + patterns: Array + metadata: { + [key: string]: unknown } -} - -export type UnknownError = { - name: "UnknownError" - data: { - message: string + always: Array + tool?: { + messageID: string + callID: string } } -export type MessageOutputLengthError = { - name: "MessageOutputLengthError" - data: { - [key: string]: unknown - } +export type EventPermissionAsked = { + type: "permission.asked" + properties: PermissionRequest } -export type MessageAbortedError = { - name: "MessageAbortedError" - data: { - message: string +export type EventPermissionReplied = { + type: "permission.replied" + properties: { + sessionID: string + requestID: string + reply: "once" | "always" | "reject" } } -export type ApiError = { - name: "APIError" - data: { - message: string - statusCode?: number - isRetryable: boolean - responseHeaders?: { - [key: string]: string +export type SessionStatus = + | { + type: "idle" } - responseBody?: string - metadata?: { - [key: string]: string + | { + type: "retry" + attempt: number + message: string + next: number } - } -} - -export type AssistantMessage = { - id: string - sessionID: string - role: "assistant" - time: { - created: number - completed?: number - } - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError - parentID: string - modelID: string - providerID: string - mode: string - agent: string - path: { - cwd: string - root: string - } - summary?: boolean - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number + | { + type: "busy" } + +export type EventSessionStatus = { + type: "session.status" + properties: { + sessionID: string + status: SessionStatus } - finish?: string } -export type Message = UserMessage | AssistantMessage - -export type EventMessageUpdated = { - type: "message.updated" +export type EventSessionIdle = { + type: "session.idle" properties: { - info: Message + sessionID: string } } -export type EventMessageRemoved = { - type: "message.removed" +export type EventSessionCompacted = { + type: "session.compacted" properties: { sessionID: string - messageID: string } } -export type TextPart = { - id: string - sessionID: string - messageID: string - type: "text" - text: string - synthetic?: boolean - ignored?: boolean - time?: { - start: number - end?: number - } - metadata?: { - [key: string]: unknown +export type EventFileEdited = { + type: "file.edited" + properties: { + file: string } } -export type ReasoningPart = { +export type Todo = { + /** + * Brief description of the task + */ + content: string + /** + * Current status of the task: pending, in_progress, completed, cancelled + */ + status: string + /** + * Priority level of the task: high, medium, low + */ + priority: string + /** + * Unique identifier for the todo item + */ id: string - sessionID: string - messageID: string - type: "reasoning" - text: string - metadata?: { - [key: string]: unknown - } - time: { - start: number - end?: number +} + +export type EventTodoUpdated = { + type: "todo.updated" + properties: { + sessionID: string + todos: Array } } -export type FilePartSourceText = { - value: string - start: number - end: number +export type EventTuiPromptAppend = { + type: "tui.prompt.append" + properties: { + text: string + } } -export type FileSource = { - text: FilePartSourceText - type: "file" - path: string +export type EventTuiCommandExecute = { + type: "tui.command.execute" + properties: { + command: + | "session.list" + | "session.new" + | "session.share" + | "session.interrupt" + | "session.compact" + | "session.page.up" + | "session.page.down" + | "session.half.page.up" + | "session.half.page.down" + | "session.first" + | "session.last" + | "prompt.clear" + | "prompt.submit" + | "agent.cycle" + | string + } } -export type Range = { - start: { - line: number - character: number +export type EventTuiToastShow = { + type: "tui.toast.show" + properties: { + title?: string + message: string + variant: "info" | "success" | "warning" | "error" + /** + * Duration in milliseconds + */ + duration?: number } - end: { - line: number - character: number +} + +export type EventTuiSessionSelect = { + type: "tui.session.select" + properties: { + /** + * Session ID to navigate to + */ + sessionID: string } } -export type SymbolSource = { - text: FilePartSourceText - type: "symbol" - path: string - range: Range - name: string - kind: number +export type EventMcpToolsChanged = { + type: "mcp.tools.changed" + properties: { + server: string + } } -export type ResourceSource = { - text: FilePartSourceText - type: "resource" - clientName: string - uri: string +export type EventCommandExecuted = { + type: "command.executed" + properties: { + name: string + sessionID: string + arguments: string + messageID: string + } } -export type FilePartSource = FileSource | SymbolSource | ResourceSource +export type PermissionAction = "allow" | "deny" | "ask" -export type FilePart = { - id: string - sessionID: string - messageID: string - type: "file" - mime: string - filename?: string - url: string - source?: FilePartSource +export type PermissionRule = { + permission: string + pattern: string + action: PermissionAction } -export type ToolStatePending = { - status: "pending" - input: { - [key: string]: unknown - } - raw: string -} +export type PermissionRuleset = Array -export type ToolStateRunning = { - status: "running" - input: { - [key: string]: unknown - } - title?: string - metadata?: { - [key: string]: unknown - } - time: { - start: number +export type Session = { + id: string + projectID: string + directory: string + parentID?: string + summary?: { + additions: number + deletions: number + files: number + diffs?: Array } -} - -export type ToolStateCompleted = { - status: "completed" - input: { - [key: string]: unknown + share?: { + url: string } - output: string title: string - metadata: { - [key: string]: unknown - } + version: string time: { - start: number - end: number - compacted?: number - } - attachments?: Array -} - -export type ToolStateError = { - status: "error" - input: { - [key: string]: unknown - } - error: string - metadata?: { - [key: string]: unknown + created: number + updated: number + compacting?: number + archived?: number } - time: { - start: number - end: number + permission?: PermissionRuleset + revert?: { + messageID: string + partID?: string + snapshot?: string + diff?: string } } -export type ToolState = ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError - -export type ToolPart = { - id: string - sessionID: string - messageID: string - type: "tool" - callID: string - tool: string - state: ToolState - metadata?: { - [key: string]: unknown +export type EventSessionCreated = { + type: "session.created" + properties: { + info: Session } } -export type StepStartPart = { - id: string - sessionID: string - messageID: string - type: "step-start" - snapshot?: string +export type EventSessionUpdated = { + type: "session.updated" + properties: { + info: Session + } } -export type StepFinishPart = { - id: string - sessionID: string - messageID: string - type: "step-finish" - reason: string - snapshot?: string - cost: number - tokens: { - input: number - output: number - reasoning: number - cache: { - read: number - write: number - } +export type EventSessionDeleted = { + type: "session.deleted" + properties: { + info: Session } } -export type SnapshotPart = { - id: string - sessionID: string - messageID: string - type: "snapshot" - snapshot: string +export type EventSessionDiff = { + type: "session.diff" + properties: { + sessionID: string + diff: Array + } } -export type PatchPart = { - id: string - sessionID: string - messageID: string - type: "patch" - hash: string - files: Array +export type EventSessionError = { + type: "session.error" + properties: { + sessionID?: string + error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError + } } -export type AgentPart = { - id: string - sessionID: string - messageID: string - type: "agent" - name: string - source?: { - value: string - start: number - end: number +export type EventFileWatcherUpdated = { + type: "file.watcher.updated" + properties: { + file: string + event: "add" | "change" | "unlink" } } -export type RetryPart = { - id: string - sessionID: string - messageID: string - type: "retry" - attempt: number - error: ApiError - time: { - created: number +export type EventVcsBranchUpdated = { + type: "vcs.branch.updated" + properties: { + branch?: string } } -export type CompactionPart = { +export type Pty = { id: string - sessionID: string - messageID: string - type: "compaction" - auto: boolean + title: string + command: string + args: Array + cwd: string + status: "running" | "exited" + pid: number } -export type Part = - | TextPart - | { - id: string - sessionID: string - messageID: string - type: "subtask" - prompt: string - description: string - agent: string - command?: string - } - | ReasoningPart - | FilePart - | ToolPart - | StepStartPart - | StepFinishPart - | SnapshotPart - | PatchPart - | AgentPart - | RetryPart - | CompactionPart - -export type EventMessagePartUpdated = { - type: "message.part.updated" +export type EventPtyCreated = { + type: "pty.created" properties: { - part: Part - delta?: string + info: Pty } } -export type EventMessagePartRemoved = { - type: "message.part.removed" +export type EventPtyUpdated = { + type: "pty.updated" properties: { - sessionID: string - messageID: string - partID: string + info: Pty } } -export type PermissionRequest = { - id: string - sessionID: string - permission: string - patterns: Array - metadata: { - [key: string]: unknown - } - always: Array - tool?: { - messageID: string - callID: string +export type EventPtyExited = { + type: "pty.exited" + properties: { + id: string + exitCode: number } } -export type EventPermissionAsked = { - type: "permission.asked" - properties: PermissionRequest +export type EventPtyDeleted = { + type: "pty.deleted" + properties: { + id: string + } } -export type EventPermissionReplied = { - type: "permission.replied" +export type EventServerConnected = { + type: "server.connected" properties: { - sessionID: string - requestID: string - reply: "once" | "always" | "reject" + [key: string]: unknown } } -export type SessionStatus = - | { - type: "idle" - } - | { - type: "retry" - attempt: number - message: string - next: number - } - | { - type: "busy" - } - -export type EventSessionStatus = { - type: "session.status" +export type EventGlobalDisposed = { + type: "global.disposed" properties: { - sessionID: string - status: SessionStatus + [key: string]: unknown } } -export type EventSessionIdle = { - type: "session.idle" - properties: { - sessionID: string +export type Event = + | EventInstallationUpdated + | EventInstallationUpdateAvailable + | EventProjectUpdated + | EventServerInstanceDisposed + | EventLspClientDiagnostics + | EventLspUpdated + | EventMessageUpdated + | EventMessageRemoved + | EventMessagePartUpdated + | EventMessagePartRemoved + | EventPermissionAsked + | EventPermissionReplied + | EventSessionStatus + | EventSessionIdle + | EventSessionCompacted + | EventFileEdited + | EventTodoUpdated + | EventTuiPromptAppend + | EventTuiCommandExecute + | EventTuiToastShow + | EventTuiSessionSelect + | EventMcpToolsChanged + | EventCommandExecuted + | EventSessionCreated + | EventSessionUpdated + | EventSessionDeleted + | EventSessionDiff + | EventSessionError + | EventFileWatcherUpdated + | EventVcsBranchUpdated + | EventPtyCreated + | EventPtyUpdated + | EventPtyExited + | EventPtyDeleted + | EventServerConnected + | EventGlobalDisposed + +export type GlobalEvent = { + directory: string + payload: Event +} + +export type BadRequestError = { + data: unknown + errors: Array<{ + [key: string]: unknown + }> + success: false +} + +export type NotFoundError = { + name: "NotFoundError" + data: { + message: string } } -export type QuestionOption = { +/** + * Custom keybind configurations + */ +export type KeybindsConfig = { + /** + * Leader key for keybind combinations + */ + leader?: string + /** + * Exit the application + */ + app_exit?: string + /** + * Open external editor + */ + editor_open?: string + /** + * List available themes + */ + theme_list?: string + /** + * Toggle sidebar + */ + sidebar_toggle?: string + /** + * Toggle session scrollbar + */ + scrollbar_toggle?: string + /** + * Toggle username visibility + */ + username_toggle?: string + /** + * View status + */ + status_view?: string + /** + * Export session to editor + */ + session_export?: string + /** + * Create a new session + */ + session_new?: string + /** + * List all sessions + */ + session_list?: string + /** + * Show session timeline + */ + session_timeline?: string + /** + * Fork session from message + */ + session_fork?: string + /** + * Rename session + */ + session_rename?: string + /** + * Share current session + */ + session_share?: string + /** + * Unshare current session + */ + session_unshare?: string + /** + * Interrupt current session + */ + session_interrupt?: string + /** + * Compact the session + */ + session_compact?: string + /** + * Scroll messages up by one page + */ + messages_page_up?: string + /** + * Scroll messages down by one page + */ + messages_page_down?: string + /** + * Scroll messages up by half page + */ + messages_half_page_up?: string + /** + * Scroll messages down by half page + */ + messages_half_page_down?: string + /** + * Navigate to first message + */ + messages_first?: string + /** + * Navigate to last message + */ + messages_last?: string + /** + * Navigate to next message + */ + messages_next?: string + /** + * Navigate to previous message + */ + messages_previous?: string + /** + * Navigate to last user message + */ + messages_last_user?: string + /** + * Copy message + */ + messages_copy?: string + /** + * Undo message + */ + messages_undo?: string + /** + * Redo message + */ + messages_redo?: string + /** + * Toggle code block concealment in messages + */ + messages_toggle_conceal?: string + /** + * Toggle tool details visibility + */ + tool_details?: string + /** + * List available models + */ + model_list?: string /** - * Display text (1-5 words, concise) + * Next recently used model */ - label: string + model_cycle_recent?: string /** - * Explanation of choice + * Previous recently used model */ - description: string -} - -export type QuestionInfo = { + model_cycle_recent_reverse?: string /** - * Complete question + * Next favorite model */ - question: string + model_cycle_favorite?: string /** - * Very short label (max 12 chars) + * Previous favorite model */ - header: string + model_cycle_favorite_reverse?: string /** - * Available choices + * List available commands */ - options: Array + command_list?: string /** - * Allow selecting multiple choices + * List agents */ - multiple?: boolean + agent_list?: string /** - * Allow typing a custom answer (default: true) + * Next agent */ - custom?: boolean -} - -export type QuestionRequest = { - id: string - sessionID: string + agent_cycle?: string /** - * Questions to ask + * Previous agent */ - questions: Array - tool?: { - messageID: string - callID: string - } -} - -export type EventQuestionAsked = { - type: "question.asked" - properties: QuestionRequest -} - -export type QuestionAnswer = Array - -export type EventQuestionReplied = { - type: "question.replied" - properties: { - sessionID: string - requestID: string - answers: Array - } -} - -export type EventQuestionRejected = { - type: "question.rejected" - properties: { - sessionID: string - requestID: string - } -} - -export type EventSessionCompacted = { - type: "session.compacted" - properties: { - sessionID: string - } + agent_cycle_reverse?: string + /** + * Cycle model variants + */ + variant_cycle?: string + /** + * Clear input field + */ + input_clear?: string + /** + * Paste from clipboard + */ + input_paste?: string + /** + * Submit input + */ + input_submit?: string + /** + * Insert newline in input + */ + input_newline?: string + /** + * Move cursor left in input + */ + input_move_left?: string + /** + * Move cursor right in input + */ + input_move_right?: string + /** + * Move cursor up in input + */ + input_move_up?: string + /** + * Move cursor down in input + */ + input_move_down?: string + /** + * Select left in input + */ + input_select_left?: string + /** + * Select right in input + */ + input_select_right?: string + /** + * Select up in input + */ + input_select_up?: string + /** + * Select down in input + */ + input_select_down?: string + /** + * Move to start of line in input + */ + input_line_home?: string + /** + * Move to end of line in input + */ + input_line_end?: string + /** + * Select to start of line in input + */ + input_select_line_home?: string + /** + * Select to end of line in input + */ + input_select_line_end?: string + /** + * Move to start of visual line in input + */ + input_visual_line_home?: string + /** + * Move to end of visual line in input + */ + input_visual_line_end?: string + /** + * Select to start of visual line in input + */ + input_select_visual_line_home?: string + /** + * Select to end of visual line in input + */ + input_select_visual_line_end?: string + /** + * Move to start of buffer in input + */ + input_buffer_home?: string + /** + * Move to end of buffer in input + */ + input_buffer_end?: string + /** + * Select to start of buffer in input + */ + input_select_buffer_home?: string + /** + * Select to end of buffer in input + */ + input_select_buffer_end?: string + /** + * Delete line in input + */ + input_delete_line?: string + /** + * Delete to end of line in input + */ + input_delete_to_line_end?: string + /** + * Delete to start of line in input + */ + input_delete_to_line_start?: string + /** + * Backspace in input + */ + input_backspace?: string + /** + * Delete character in input + */ + input_delete?: string + /** + * Undo in input + */ + input_undo?: string + /** + * Redo in input + */ + input_redo?: string + /** + * Move word forward in input + */ + input_word_forward?: string + /** + * Move word backward in input + */ + input_word_backward?: string + /** + * Select word forward in input + */ + input_select_word_forward?: string + /** + * Select word backward in input + */ + input_select_word_backward?: string + /** + * Delete word forward in input + */ + input_delete_word_forward?: string + /** + * Delete word backward in input + */ + input_delete_word_backward?: string + /** + * Previous history item + */ + history_previous?: string + /** + * Next history item + */ + history_next?: string + /** + * Next child session + */ + session_child_cycle?: string + /** + * Previous child session + */ + session_child_cycle_reverse?: string + /** + * Go to parent session + */ + session_parent?: string + /** + * Suspend terminal + */ + terminal_suspend?: string + /** + * Toggle terminal title + */ + terminal_title_toggle?: string + /** + * Toggle tips on home screen + */ + tips_toggle?: string } -export type EventFileEdited = { - type: "file.edited" - properties: { - file: string - } -} +/** + * Log level + */ +export type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" -export type Todo = { +/** + * Server configuration for opencode serve and web commands + */ +export type ServerConfig = { /** - * Brief description of the task + * Port to listen on */ - content: string + port?: number /** - * Current status of the task: pending, in_progress, completed, cancelled + * Hostname to listen on */ - status: string + hostname?: string /** - * Priority level of the task: high, medium, low + * Enable mDNS service discovery */ - priority: string + mdns?: boolean /** - * Unique identifier for the todo item + * Additional domains to allow for CORS */ - id: string -} - -export type EventTodoUpdated = { - type: "todo.updated" - properties: { - sessionID: string - todos: Array - } -} - -export type EventSkillUpdated = { - type: "skill.updated" - properties: { - [key: string]: unknown - } + cors?: Array } -export type EventTuiPromptAppend = { - type: "tui.prompt.append" - properties: { - text: string - } -} +export type PermissionActionConfig = "ask" | "allow" | "deny" -export type EventTuiCommandExecute = { - type: "tui.command.execute" - properties: { - command: - | "session.list" - | "session.new" - | "session.share" - | "session.interrupt" - | "session.compact" - | "session.page.up" - | "session.page.down" - | "session.half.page.up" - | "session.half.page.down" - | "session.first" - | "session.last" - | "prompt.clear" - | "prompt.submit" - | "agent.cycle" - | string - } +export type PermissionObjectConfig = { + [key: string]: PermissionActionConfig } -export type EventTuiToastShow = { - type: "tui.toast.show" - properties: { - title?: string - message: string - variant: "info" | "success" | "warning" | "error" - /** - * Duration in milliseconds - */ - duration?: number - } -} +export type PermissionRuleConfig = PermissionActionConfig | PermissionObjectConfig -export type EventTuiSessionSelect = { - type: "tui.session.select" - properties: { - /** - * Session ID to navigate to - */ - sessionID: string - } -} +export type PermissionConfig = + | { + __originalKeys?: Array + read?: PermissionRuleConfig + edit?: PermissionRuleConfig + glob?: PermissionRuleConfig + grep?: PermissionRuleConfig + list?: PermissionRuleConfig + bash?: PermissionRuleConfig + task?: PermissionRuleConfig + external_directory?: PermissionRuleConfig + todowrite?: PermissionActionConfig + todoread?: PermissionActionConfig + webfetch?: PermissionActionConfig + websearch?: PermissionActionConfig + codesearch?: PermissionActionConfig + lsp?: PermissionRuleConfig + doom_loop?: PermissionActionConfig + [key: string]: PermissionRuleConfig | Array | PermissionActionConfig | undefined + } + | PermissionActionConfig -export type EventMcpToolsChanged = { - type: "mcp.tools.changed" - properties: { - server: string +export type AgentConfig = { + model?: string + temperature?: number + top_p?: number + prompt?: string + /** + * @deprecated Use 'permission' field instead + */ + tools?: { + [key: string]: boolean } -} - -export type EventCommandUpdated = { - type: "command.updated" - properties: { + disable?: boolean + /** + * Description of when to use the agent + */ + description?: string + mode?: "subagent" | "primary" | "all" + /** + * Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent) + */ + hidden?: boolean + options?: { [key: string]: unknown } + /** + * Hex color code for the agent (e.g., #FF5733) + */ + color?: string + /** + * Maximum number of agentic iterations before forcing text-only response + */ + steps?: number + /** + * @deprecated Use 'steps' field instead. + */ + maxSteps?: number + permission?: PermissionConfig + [key: string]: + | unknown + | string + | number + | { + [key: string]: boolean + } + | boolean + | "subagent" + | "primary" + | "all" + | { + [key: string]: unknown + } + | string + | number + | PermissionConfig + | undefined } -export type EventCommandExecuted = { - type: "command.executed" - properties: { - name: string - sessionID: string - arguments: string - messageID: string - } -} - -export type PermissionAction = "allow" | "deny" | "ask" - -export type PermissionRule = { - permission: string - pattern: string - action: PermissionAction -} - -export type PermissionRuleset = Array - -export type Session = { - id: string - slug: string - projectID: string - directory: string - parentID?: string - summary?: { - additions: number - deletions: number - files: number - diffs?: Array - } - share?: { - url: string - } - title: string - version: string - time: { - created: number - updated: number - compacting?: number - archived?: number +export type ProviderConfig = { + api?: string + name?: string + env?: Array + id?: string + npm?: string + models?: { + [key: string]: { + id?: string + name?: string + family?: string + release_date?: string + attachment?: boolean + reasoning?: boolean + temperature?: boolean + tool_call?: boolean + interleaved?: + | true + | { + field: "reasoning_content" | "reasoning_details" + } + cost?: { + input: number + output: number + cache_read?: number + cache_write?: number + context_over_200k?: { + input: number + output: number + cache_read?: number + cache_write?: number + } + } + limit?: { + context: number + output: number + } + modalities?: { + input: Array<"text" | "audio" | "image" | "video" | "pdf"> + output: Array<"text" | "audio" | "image" | "video" | "pdf"> + } + experimental?: boolean + status?: "alpha" | "beta" | "deprecated" + options?: { + [key: string]: unknown + } + headers?: { + [key: string]: string + } + provider?: { + npm: string + } + /** + * Variant-specific configuration + */ + variants?: { + [key: string]: { + /** + * Disable this variant for the model + */ + disabled?: boolean + [key: string]: unknown | boolean | undefined + } + } + } } - permission?: PermissionRuleset - revert?: { - messageID: string - partID?: string - snapshot?: string - diff?: string + whitelist?: Array + blacklist?: Array + options?: { + apiKey?: string + baseURL?: string + /** + * GitHub Enterprise URL for copilot authentication + */ + enterpriseUrl?: string + /** + * Enable promptCacheKey for this provider (default false) + */ + setCacheKey?: boolean + /** + * Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout. + */ + timeout?: number | false + [key: string]: unknown | string | boolean | number | false | undefined } } -export type EventSessionCreated = { - type: "session.created" - properties: { - info: Session +export type McpLocalConfig = { + /** + * Type of MCP server connection + */ + type: "local" + /** + * Command and arguments to run the MCP server + */ + command: Array + /** + * Environment variables to set when running the MCP server + */ + environment?: { + [key: string]: string } + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number } -export type EventSessionUpdated = { - type: "session.updated" - properties: { - info: Session - } +export type McpOAuthConfig = { + /** + * OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted. + */ + clientId?: string + /** + * OAuth client secret (if required by the authorization server) + */ + clientSecret?: string + /** + * OAuth scopes to request during authorization + */ + scope?: string } -export type EventSessionDeleted = { - type: "session.deleted" - properties: { - info: Session +export type McpRemoteConfig = { + /** + * Type of MCP server connection + */ + type: "remote" + /** + * URL of the remote MCP server + */ + url: string + /** + * Enable or disable the MCP server on startup + */ + enabled?: boolean + /** + * Headers to send with the request + */ + headers?: { + [key: string]: string } + /** + * OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection. + */ + oauth?: McpOAuthConfig | false + /** + * Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified. + */ + timeout?: number } -export type EventSessionDiff = { - type: "session.diff" - properties: { - sessionID: string - diff: Array - } -} +/** + * @deprecated Always uses stretch layout. + */ +export type LayoutConfig = "auto" | "stretch" -export type EventSessionError = { - type: "session.error" - properties: { - sessionID?: string - error?: ProviderAuthError | UnknownError | MessageOutputLengthError | MessageAbortedError | ApiError +export type Config = { + /** + * JSON schema reference for configuration validation + */ + $schema?: string + /** + * Theme name to use for the interface + */ + theme?: string + keybinds?: KeybindsConfig + logLevel?: LogLevel + /** + * TUI specific settings + */ + tui?: { + /** + * TUI scroll speed + */ + scroll_speed?: number + /** + * Scroll acceleration settings + */ + scroll_acceleration?: { + /** + * Enable scroll acceleration + */ + enabled: boolean + } + /** + * Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column + */ + diff_style?: "auto" | "stacked" } -} - -export type EventVcsBranchUpdated = { - type: "vcs.branch.updated" - properties: { - branch?: string + server?: ServerConfig + /** + * Command configuration, see https://opencode.ai/docs/commands + */ + command?: { + [key: string]: { + template: string + description?: string + agent?: string + model?: string + subtask?: boolean + } } -} - -export type Pty = { - id: string - title: string - command: string - args: Array - cwd: string - status: "running" | "exited" - pid: number -} - -export type EventPtyCreated = { - type: "pty.created" - properties: { - info: Pty + watcher?: { + ignore?: Array } -} - -export type EventPtyUpdated = { - type: "pty.updated" - properties: { - info: Pty + plugin?: Array + snapshot?: boolean + /** + * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing + */ + share?: "manual" | "auto" | "disabled" + /** + * @deprecated Use 'share' field instead. Share newly created sessions automatically + */ + autoshare?: boolean + /** + * Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications + */ + autoupdate?: boolean | "notify" + /** + * Disable providers that are loaded automatically + */ + disabled_providers?: Array + /** + * When set, ONLY these providers will be enabled. All other providers will be ignored + */ + enabled_providers?: Array + /** + * Model to use in the format of provider/model, eg anthropic/claude-2 + */ + model?: string + /** + * Small model to use for tasks like title generation in the format of provider/model + */ + small_model?: string + /** + * Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid. + */ + default_agent?: string + /** + * Custom username to display in conversations instead of system username + */ + username?: string + /** + * @deprecated Use `agent` field instead. + */ + mode?: { + build?: AgentConfig + plan?: AgentConfig + [key: string]: AgentConfig | undefined } -} - -export type EventPtyExited = { - type: "pty.exited" - properties: { - id: string - exitCode: number + /** + * Agent configuration, see https://opencode.ai/docs/agent + */ + agent?: { + plan?: AgentConfig + build?: AgentConfig + general?: AgentConfig + explore?: AgentConfig + title?: AgentConfig + summary?: AgentConfig + compaction?: AgentConfig + [key: string]: AgentConfig | undefined + } + /** + * Custom provider configurations and model overrides + */ + provider?: { + [key: string]: ProviderConfig } -} - -export type EventPtyDeleted = { - type: "pty.deleted" - properties: { - id: string + /** + * MCP (Model Context Protocol) server configurations + */ + mcp?: { + [key: string]: + | McpLocalConfig + | McpRemoteConfig + | { + enabled: boolean + } } -} - -export type EventServerConnected = { - type: "server.connected" - properties: { - [key: string]: unknown + formatter?: + | false + | { + [key: string]: { + disabled?: boolean + command?: Array + environment?: { + [key: string]: string + } + extensions?: Array + } + } + lsp?: + | false + | { + [key: string]: + | { + disabled: true + } + | { + command: Array + extensions?: Array + disabled?: boolean + env?: { + [key: string]: string + } + initialization?: { + [key: string]: unknown + } + } + } + /** + * Additional instruction files or patterns to include + */ + instructions?: Array + layout?: LayoutConfig + permission?: PermissionConfig + tools?: { + [key: string]: boolean } -} - -export type EventGlobalDisposed = { - type: "global.disposed" - properties: { - [key: string]: unknown + enterprise?: { + /** + * Enterprise URL + */ + url?: string } -} - -export type Event = - | EventInstallationUpdated - | EventInstallationUpdateAvailable - | EventProjectUpdated - | EventServerInstanceDisposed - | EventFileWatcherUpdated - | EventConfigUpdated - | EventLspClientDiagnostics - | EventLspUpdated - | EventMessageUpdated - | EventMessageRemoved - | EventMessagePartUpdated - | EventMessagePartRemoved - | EventPermissionAsked - | EventPermissionReplied - | EventSessionStatus - | EventSessionIdle - | EventQuestionAsked - | EventQuestionReplied - | EventQuestionRejected - | EventSessionCompacted - | EventFileEdited - | EventTodoUpdated - | EventSkillUpdated - | EventTuiPromptAppend - | EventTuiCommandExecute - | EventTuiToastShow - | EventTuiSessionSelect - | EventMcpToolsChanged - | EventCommandUpdated - | EventCommandExecuted - | EventSessionCreated - | EventSessionUpdated - | EventSessionDeleted - | EventSessionDiff - | EventSessionError - | EventVcsBranchUpdated - | EventPtyCreated - | EventPtyUpdated - | EventPtyExited - | EventPtyDeleted - | EventServerConnected - | EventGlobalDisposed - -export type GlobalEvent = { - directory: string - payload: Event -} - -export type BadRequestError = { - data: unknown - errors: Array<{ - [key: string]: unknown - }> - success: false -} - -export type NotFoundError = { - name: "NotFoundError" - data: { - message: string + compaction?: { + /** + * Enable automatic compaction when context is full (default: true) + */ + auto?: boolean + /** + * Enable pruning of old tool outputs (default: true) + */ + prune?: boolean + } + experimental?: { + hook?: { + file_edited?: { + [key: string]: Array<{ + command: Array + environment?: { + [key: string]: string + } + }> + } + session_completed?: Array<{ + command: Array + environment?: { + [key: string]: string + } + }> + } + /** + * Number of retries for chat completions on failure + */ + chatMaxRetries?: number + disable_paste_summary?: boolean + /** + * Enable the batch tool + */ + batch_tool?: boolean + /** + * Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag) + */ + openTelemetry?: boolean + /** + * Tools that should only be available to primary agents. + */ + primary_tools?: Array + /** + * Continue the agent loop when a tool call is denied + */ + continue_loop_on_deny?: boolean + /** + * Timeout in milliseconds for model context protocol (MCP) requests + */ + mcp_timeout?: number } } @@ -1942,7 +1827,6 @@ export type Model = { } limit: { context: number - input?: number output: number } status: "alpha" | "beta" | "deprecated" | "active" @@ -2107,7 +1991,6 @@ export type OAuth = { refresh: string access: string expires: number - accountId?: string enterpriseUrl?: string } @@ -2629,14 +2512,7 @@ export type SessionListData = { body?: never path?: never query?: { - /** - * Filter sessions by project directory - */ directory?: string - /** - * Only return root sessions (no parentID) - */ - roots?: boolean /** * Filter sessions updated on or after this timestamp (milliseconds since epoch) */ @@ -3669,95 +3545,6 @@ export type PermissionListResponses = { export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses] -export type QuestionListData = { - body?: never - path?: never - query?: { - directory?: string - } - url: "/question" -} - -export type QuestionListResponses = { - /** - * List of pending questions - */ - 200: Array -} - -export type QuestionListResponse = QuestionListResponses[keyof QuestionListResponses] - -export type QuestionReplyData = { - body?: { - /** - * User answers in order of questions (each answer is an array of selected labels) - */ - answers: Array - } - path: { - requestID: string - } - query?: { - directory?: string - } - url: "/question/{requestID}/reply" -} - -export type QuestionReplyErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type QuestionReplyError = QuestionReplyErrors[keyof QuestionReplyErrors] - -export type QuestionReplyResponses = { - /** - * Question answered successfully - */ - 200: boolean -} - -export type QuestionReplyResponse = QuestionReplyResponses[keyof QuestionReplyResponses] - -export type QuestionRejectData = { - body?: never - path: { - requestID: string - } - query?: { - directory?: string - } - url: "/question/{requestID}/reject" -} - -export type QuestionRejectErrors = { - /** - * Bad request - */ - 400: BadRequestError - /** - * Not found - */ - 404: NotFoundError -} - -export type QuestionRejectError = QuestionRejectErrors[keyof QuestionRejectErrors] - -export type QuestionRejectResponses = { - /** - * Question rejected successfully - */ - 200: boolean -} - -export type QuestionRejectResponse = QuestionRejectResponses[keyof QuestionRejectResponses] - export type CommandListData = { body?: never path?: never @@ -3848,7 +3635,6 @@ export type ProviderListResponses = { } limit: { context: number - input?: number output: number } modalities?: {