Merge branch 'dev' into truncate-to-file
This commit is contained in:
commit
ff2c1c4267
47 changed files with 639 additions and 105 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/app",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -248,6 +248,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const isFocused = createFocusSignal(() => editorRef)
|
||||
|
||||
createEffect(() => {
|
||||
params.id
|
||||
editorRef.focus()
|
||||
|
|
@ -258,7 +260,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
onCleanup(() => clearInterval(interval))
|
||||
})
|
||||
|
||||
const isFocused = createFocusSignal(() => editorRef)
|
||||
const [composing, setComposing] = createSignal(false)
|
||||
const isImeComposing = (event: KeyboardEvent) => event.isComposing || composing() || event.keyCode === 229
|
||||
|
||||
|
|
@ -292,12 +293,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
const clipboardData = event.clipboardData
|
||||
if (!clipboardData) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const items = Array.from(clipboardData.items)
|
||||
const imageItems = items.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type))
|
||||
|
||||
if (imageItems.length > 0) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
for (const item of imageItems) {
|
||||
const file = item.getAsFile()
|
||||
if (file) await addImageAttachment(file)
|
||||
|
|
@ -305,8 +307,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const plainText = clipboardData.getData("text/plain") ?? ""
|
||||
addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
||||
}
|
||||
|
|
@ -347,13 +347,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
}
|
||||
|
||||
onMount(() => {
|
||||
editorRef.addEventListener("paste", handlePaste)
|
||||
document.addEventListener("dragover", handleGlobalDragOver)
|
||||
document.addEventListener("dragleave", handleGlobalDragLeave)
|
||||
document.addEventListener("drop", handleGlobalDrop)
|
||||
})
|
||||
onCleanup(() => {
|
||||
editorRef.removeEventListener("paste", handlePaste)
|
||||
document.removeEventListener("dragover", handleGlobalDragOver)
|
||||
document.removeEventListener("dragleave", handleGlobalDragLeave)
|
||||
document.removeEventListener("drop", handleGlobalDrop)
|
||||
|
|
@ -1508,6 +1506,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||
}}
|
||||
contenteditable="true"
|
||||
onInput={handleInput}
|
||||
onPaste={handlePaste}
|
||||
onCompositionStart={() => setComposing(true)}
|
||||
onCompositionEnd={() => setComposing(false)}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-app",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/console-core",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-function",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/console-mail",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"dependencies": {
|
||||
"@jsx-email/all": "2.2.3",
|
||||
"@jsx-email/cli": "1.4.3",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@opencode-ai/desktop",
|
||||
"private": true,
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -12,5 +12,19 @@
|
|||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<key>com.apple.security.automation.apple-events</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.audio-input</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.camera</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.addressbook</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.calendars</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.location</key>
|
||||
<true/>
|
||||
<key>com.apple.security.personal-information.photos-library</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild {
|
|||
.args([
|
||||
"-il",
|
||||
"-c",
|
||||
&format!("{} serve --port={}", sidecar.display(), port),
|
||||
&format!("\"{}\" serve --port={}", sidecar.display(), port),
|
||||
])
|
||||
.spawn()
|
||||
.expect("Failed to spawn opencode")
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { Suspense, createResource, ParentProps } from "solid-js"
|
|||
import { UPDATER_ENABLED } from "./updater"
|
||||
import { createMenu } from "./menu"
|
||||
import pkg from "../package.json"
|
||||
import { Show } from "solid-js"
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
|
||||
|
|
@ -288,7 +289,9 @@ function ServerGate(props: ParentProps) {
|
|||
})
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
// Not using suspense as not all components are compatible with it (undefined refs)
|
||||
<Show
|
||||
when={status.state !== "pending"}
|
||||
fallback={
|
||||
<div class="h-screen w-screen flex flex-col items-center justify-center bg-background-base">
|
||||
<Logo class="w-xl opacity-12 animate-pulse" />
|
||||
|
|
@ -296,9 +299,9 @@ function ServerGate(props: ParentProps) {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
{/* Triggers suspense/error boundaries without rendering the returned value */}
|
||||
{/* Trigger error boundary without rendering the returned value */}
|
||||
{(status(), null)}
|
||||
<Suspense>{props.children}</Suspense>
|
||||
</Suspense>
|
||||
{props.children}
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/enterprise",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "opencode"
|
||||
name = "OpenCode"
|
||||
description = "The open source coding agent."
|
||||
version = "1.1.4"
|
||||
version = "1.1.6"
|
||||
schema_version = 1
|
||||
authors = ["Anomaly"]
|
||||
repository = "https://github.com/anomalyco/opencode"
|
||||
|
|
@ -11,26 +11,26 @@ name = "OpenCode"
|
|||
icon = "./icons/opencode.svg"
|
||||
|
||||
[agent_servers.opencode.targets.darwin-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-darwin-arm64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-darwin-arm64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.darwin-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-darwin-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-darwin-x64.zip"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-aarch64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-linux-arm64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-linux-arm64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.linux-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-linux-x64.tar.gz"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-linux-x64.tar.gz"
|
||||
cmd = "./opencode"
|
||||
args = ["acp"]
|
||||
|
||||
[agent_servers.opencode.targets.windows-x86_64]
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.4/opencode-windows-x64.zip"
|
||||
archive = "https://github.com/anomalyco/opencode/releases/download/v1.1.6/opencode-windows-x64.zip"
|
||||
cmd = "./opencode.exe"
|
||||
args = ["acp"]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/function",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
"@clack/prompts": "1.0.0-alpha.1",
|
||||
"@hono/standard-validator": "0.1.5",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"@modelcontextprotocol/sdk": "1.15.1",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
"@octokit/graphql": "9.0.2",
|
||||
"@octokit/rest": "catalog:",
|
||||
"@openauthjs/openauth": "catalog:",
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ export namespace Agent {
|
|||
options: {},
|
||||
native: true,
|
||||
hidden: true,
|
||||
temperature: 0.5,
|
||||
permission: PermissionNext.merge(
|
||||
defaults,
|
||||
PermissionNext.fromConfig({
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@ Your output must be:
|
|||
</task>
|
||||
|
||||
<rules>
|
||||
- Title must be grammatically correct and read naturally - no word salad
|
||||
- Never include tool names in the title (e.g. "read tool", "bash tool", "edit tool")
|
||||
- Focus on the main topic or question the user needs to retrieve
|
||||
- Use -ing verbs for actions (Debugging, Implementing, Analyzing)
|
||||
- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing"
|
||||
- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it
|
||||
- Keep exact: technical terms, numbers, filenames, HTTP codes
|
||||
- Remove: the, this, my, a, an
|
||||
- Never assume tech stack
|
||||
|
|
@ -29,8 +32,12 @@ Your output must be:
|
|||
<examples>
|
||||
"debug 500 errors in production" → Debugging production 500 errors
|
||||
"refactor user service" → Refactoring user service
|
||||
"why is app.js failing" → Analyzing app.js failure
|
||||
"implement rate limiting" → Implementing rate limiting
|
||||
"how do I connect postgres to my API" → Connecting Postgres to API
|
||||
"why is app.js failing" → app.js failure investigation
|
||||
"implement rate limiting" → Rate limiting implementation
|
||||
"how do I connect postgres to my API" → Postgres API connection
|
||||
"best practices for React hooks" → React hooks best practices
|
||||
"@src/auth.ts can you add refresh token support" → Auth refresh token support
|
||||
"@utils/parser.ts this is broken" → Parser bug fix
|
||||
"look at @config.json" → Config review
|
||||
"@App.tsx add dark mode toggle" → Dark mode toggle in App
|
||||
</examples>
|
||||
|
|
|
|||
|
|
@ -653,8 +653,10 @@ export function Autocomplete(props: {
|
|||
})
|
||||
|
||||
const height = createMemo(() => {
|
||||
if (options().length) return Math.min(10, options().length)
|
||||
return 1
|
||||
const count = options().length || 1
|
||||
if (!store.visible) return Math.min(10, count)
|
||||
positionTick()
|
||||
return Math.min(10, count, Math.max(1, props.anchor().y))
|
||||
})
|
||||
|
||||
let scroll: ScrollBoxRenderable
|
||||
|
|
|
|||
|
|
@ -288,11 +288,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
|
||||
createEffect(() => {
|
||||
const theme = sync.data.config.theme
|
||||
console.log("theme", theme)
|
||||
if (theme) setStore("active", theme)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
function init() {
|
||||
resolveSystemTheme()
|
||||
getCustomThemes()
|
||||
.then((custom) => {
|
||||
setStore(
|
||||
|
|
@ -309,15 +309,18 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
setStore("ready", true)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMount(init)
|
||||
|
||||
function resolveSystemTheme() {
|
||||
console.log("resolved system theme")
|
||||
console.log("resolveSystemTheme")
|
||||
renderer
|
||||
.getPalette({
|
||||
size: 16,
|
||||
})
|
||||
.then((colors) => {
|
||||
console.log(colors.palette)
|
||||
if (!colors.palette[0]) {
|
||||
if (store.active === "system") {
|
||||
setStore(
|
||||
|
|
@ -341,11 +344,9 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
|||
}
|
||||
|
||||
const renderer = useRenderer()
|
||||
resolveSystemTheme()
|
||||
|
||||
const sdk = useSDK()
|
||||
sdk.event.on("server.instance.disposed", () => {
|
||||
resolveSystemTheme()
|
||||
process.on("SIGUSR2", async () => {
|
||||
renderer.clearPaletteCache()
|
||||
init()
|
||||
})
|
||||
|
||||
const values = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@
|
|||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"selectedListItemText": {
|
||||
"dark": "#0a0a0a",
|
||||
"light": "#ffffff"
|
||||
},
|
||||
"background": {
|
||||
"dark": "transparent",
|
||||
"light": "transparent"
|
||||
|
|
|
|||
|
|
@ -77,6 +77,10 @@
|
|||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"selectedListItemText": {
|
||||
"dark": "#0a0a0a",
|
||||
"light": "#ffffff"
|
||||
},
|
||||
"background": {
|
||||
"dark": "darkStep1",
|
||||
"light": "lightStep1"
|
||||
|
|
|
|||
|
|
@ -37,14 +37,40 @@ export namespace Config {
|
|||
|
||||
export const state = Instance.state(async () => {
|
||||
const auth = await Auth.all()
|
||||
let result = await global()
|
||||
|
||||
// Override with custom config if provided
|
||||
// Load remote/well-known config first as the base layer (lowest precedence)
|
||||
// This allows organizations to provide default configs that users can override
|
||||
let result: Info = {}
|
||||
for (const [key, value] of Object.entries(auth)) {
|
||||
if (value.type === "wellknown") {
|
||||
process.env[value.key] = value.token
|
||||
log.debug("fetching remote config", { url: `${key}/.well-known/opencode` })
|
||||
const response = await fetch(`${key}/.well-known/opencode`)
|
||||
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 ?? {}
|
||||
// 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"
|
||||
result = mergeConfigConcatArrays(
|
||||
result,
|
||||
await load(JSON.stringify(remoteConfig), `${key}/.well-known/opencode`),
|
||||
)
|
||||
log.debug("loaded remote config from well-known", { url: key })
|
||||
}
|
||||
}
|
||||
|
||||
// Global user config overrides remote config
|
||||
result = mergeConfigConcatArrays(result, await global())
|
||||
|
||||
// Custom config path overrides global
|
||||
if (Flag.OPENCODE_CONFIG) {
|
||||
result = mergeConfigConcatArrays(result, await loadFile(Flag.OPENCODE_CONFIG))
|
||||
log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
|
||||
}
|
||||
|
||||
// Project config has highest precedence (overrides global and remote)
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree)
|
||||
for (const resolved of found.toReversed()) {
|
||||
|
|
@ -52,19 +78,12 @@ export namespace Config {
|
|||
}
|
||||
}
|
||||
|
||||
// Inline config content has highest precedence
|
||||
if (Flag.OPENCODE_CONFIG_CONTENT) {
|
||||
result = mergeConfigConcatArrays(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT))
|
||||
log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(auth)) {
|
||||
if (value.type === "wellknown") {
|
||||
process.env[value.key] = value.token
|
||||
const wellknown = (await fetch(`${key}/.well-known/opencode`).then((x) => x.json())) as any
|
||||
result = mergeConfigConcatArrays(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd()))
|
||||
}
|
||||
}
|
||||
|
||||
result.agent = result.agent || {}
|
||||
result.mode = result.mode || {}
|
||||
result.plugin = result.plugin || []
|
||||
|
|
|
|||
|
|
@ -111,8 +111,8 @@ export namespace Installation {
|
|||
)
|
||||
|
||||
async function getBrewFormula() {
|
||||
const tapFormula = await $`brew list --formula sst/tap/opencode`.throws(false).quiet().text()
|
||||
if (tapFormula.includes("opencode")) return "sst/tap/opencode"
|
||||
const tapFormula = await $`brew list --formula anomalyco/tap/opencode`.throws(false).quiet().text()
|
||||
if (tapFormula.includes("opencode")) return "anomalyco/tap/opencode"
|
||||
const coreFormula = await $`brew list --formula opencode`.throws(false).quiet().text()
|
||||
if (coreFormula.includes("opencode")) return "opencode"
|
||||
return "opencode"
|
||||
|
|
|
|||
|
|
@ -246,7 +246,12 @@ export namespace Server {
|
|||
},
|
||||
)
|
||||
.use(async (c, next) => {
|
||||
const directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
|
||||
let directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
|
||||
try {
|
||||
directory = decodeURIComponent(directory)
|
||||
} catch {
|
||||
// fallback to original value
|
||||
}
|
||||
return Instance.provide({
|
||||
directory,
|
||||
init: InstanceBootstrap,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import { test, expect, mock, afterEach } from "bun:test"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
|
|
@ -913,3 +914,234 @@ test("permission config preserves key order", async () => {
|
|||
},
|
||||
})
|
||||
})
|
||||
|
||||
// MCP config merging tests
|
||||
|
||||
test("project config can override MCP server enabled status", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
// Simulates a base config (like from remote .well-known) with disabled MCP
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.jsonc"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
jira: {
|
||||
type: "remote",
|
||||
url: "https://jira.example.com/mcp",
|
||||
enabled: false,
|
||||
},
|
||||
wiki: {
|
||||
type: "remote",
|
||||
url: "https://wiki.example.com/mcp",
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
// Project config enables just jira
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
jira: {
|
||||
type: "remote",
|
||||
url: "https://jira.example.com/mcp",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
// jira should be enabled (overridden by project config)
|
||||
expect(config.mcp?.jira).toEqual({
|
||||
type: "remote",
|
||||
url: "https://jira.example.com/mcp",
|
||||
enabled: true,
|
||||
})
|
||||
// wiki should still be disabled (not overridden)
|
||||
expect(config.mcp?.wiki).toEqual({
|
||||
type: "remote",
|
||||
url: "https://wiki.example.com/mcp",
|
||||
enabled: false,
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("MCP config deep merges preserving base config properties", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
// Base config with full MCP definition
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.jsonc"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
myserver: {
|
||||
type: "remote",
|
||||
url: "https://myserver.example.com/mcp",
|
||||
enabled: false,
|
||||
headers: {
|
||||
"X-Custom-Header": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
// Override just enables it, should preserve other properties
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
myserver: {
|
||||
type: "remote",
|
||||
url: "https://myserver.example.com/mcp",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
expect(config.mcp?.myserver).toEqual({
|
||||
type: "remote",
|
||||
url: "https://myserver.example.com/mcp",
|
||||
enabled: true,
|
||||
headers: {
|
||||
"X-Custom-Header": "value",
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("local .opencode config can override MCP from project config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
// Project config with disabled MCP
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
docs: {
|
||||
type: "remote",
|
||||
url: "https://docs.example.com/mcp",
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
// Local .opencode directory config enables it
|
||||
const opencodeDir = path.join(dir, ".opencode")
|
||||
await fs.mkdir(opencodeDir, { recursive: true })
|
||||
await Bun.write(
|
||||
path.join(opencodeDir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
docs: {
|
||||
type: "remote",
|
||||
url: "https://docs.example.com/mcp",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
expect(config.mcp?.docs?.enabled).toBe(true)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("project config overrides remote well-known config", async () => {
|
||||
const originalFetch = globalThis.fetch
|
||||
let fetchedUrl: string | undefined
|
||||
const mockFetch = mock((url: string | URL | Request) => {
|
||||
const urlStr = url.toString()
|
||||
if (urlStr.includes(".well-known/opencode")) {
|
||||
fetchedUrl = urlStr
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
config: {
|
||||
mcp: {
|
||||
jira: {
|
||||
type: "remote",
|
||||
url: "https://jira.example.com/mcp",
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
{ status: 200 },
|
||||
),
|
||||
)
|
||||
}
|
||||
return originalFetch(url)
|
||||
})
|
||||
globalThis.fetch = mockFetch as unknown as typeof fetch
|
||||
|
||||
const originalAuthAll = Auth.all
|
||||
Auth.all = mock(() =>
|
||||
Promise.resolve({
|
||||
"https://example.com": {
|
||||
type: "wellknown" as const,
|
||||
key: "TEST_TOKEN",
|
||||
token: "test-token",
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
try {
|
||||
await using tmp = await tmpdir({
|
||||
git: true,
|
||||
init: async (dir) => {
|
||||
// Project config enables jira (overriding remote default)
|
||||
await Bun.write(
|
||||
path.join(dir, "opencode.json"),
|
||||
JSON.stringify({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
mcp: {
|
||||
jira: {
|
||||
type: "remote",
|
||||
url: "https://jira.example.com/mcp",
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
},
|
||||
})
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await Config.get()
|
||||
// Verify fetch was called for wellknown config
|
||||
expect(fetchedUrl).toBe("https://example.com/.well-known/opencode")
|
||||
// Project config (enabled: true) should override remote (enabled: false)
|
||||
expect(config.mcp?.jira?.enabled).toBe(true)
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch
|
||||
Auth.all = originalAuthAll
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/plugin",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/sdk",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ export function createOpencodeClient(config?: Config & { directory?: string }) {
|
|||
}
|
||||
|
||||
if (config?.directory) {
|
||||
const isNonASCII = /[^\x00-\x7F]/.test(config.directory)
|
||||
const encodedDirectory = isNonASCII ? encodeURIComponent(config.directory) : config.directory
|
||||
config.headers = {
|
||||
...config.headers,
|
||||
"x-opencode-directory": config.directory,
|
||||
"x-opencode-directory": encodedDirectory,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/slack",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/ui",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"exports": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import fuzzysort from "fuzzysort"
|
||||
import { entries, flatMap, groupBy, map, pipe } from "remeda"
|
||||
import { createMemo, createResource } from "solid-js"
|
||||
import { createEffect, createMemo, createResource, on } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { createList } from "solid-list"
|
||||
|
||||
|
|
@ -86,9 +86,14 @@ export function useFilteredList<T>(props: FilteredListProps<T>) {
|
|||
}
|
||||
}
|
||||
|
||||
createEffect(
|
||||
on(grouped, () => {
|
||||
reset()
|
||||
}),
|
||||
)
|
||||
|
||||
const onInput = (value: string) => {
|
||||
setStore("filter", value)
|
||||
reset()
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -372,3 +372,17 @@ input:where([type="button"], [type="reset"], [type="submit"]),
|
|||
[hidden]:where(:not([hidden="until-found"])) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent iOS Safari from auto-zooming on input focus.
|
||||
iOS WebKit zooms on any input with font-size < 16px as an accessibility feature.
|
||||
*/
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
input,
|
||||
select,
|
||||
textarea,
|
||||
[contenteditable="true"] {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import catppuccinThemeJson from "./themes/catppuccin.json"
|
|||
import ayuThemeJson from "./themes/ayu.json"
|
||||
import oneDarkProThemeJson from "./themes/onedarkpro.json"
|
||||
import shadesOfPurpleThemeJson from "./themes/shadesofpurple.json"
|
||||
import nightowlThemeJson from "./themes/nightowl.json"
|
||||
|
||||
export const oc1Theme = oc1ThemeJson as DesktopTheme
|
||||
export const tokyonightTheme = tokyoThemeJson as DesktopTheme
|
||||
|
|
@ -20,6 +21,7 @@ export const catppuccinTheme = catppuccinThemeJson as DesktopTheme
|
|||
export const ayuTheme = ayuThemeJson as DesktopTheme
|
||||
export const oneDarkProTheme = oneDarkProThemeJson as DesktopTheme
|
||||
export const shadesOfPurpleTheme = shadesOfPurpleThemeJson as DesktopTheme
|
||||
export const nightowlTheme = nightowlThemeJson as DesktopTheme
|
||||
|
||||
export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
|
||||
"oc-1": oc1Theme,
|
||||
|
|
@ -32,4 +34,5 @@ export const DEFAULT_THEMES: Record<string, DesktopTheme> = {
|
|||
ayu: ayuTheme,
|
||||
onedarkpro: oneDarkProTheme,
|
||||
shadesofpurple: shadesOfPurpleTheme,
|
||||
nightowl: nightowlTheme,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,4 +41,5 @@ export {
|
|||
ayuTheme,
|
||||
oneDarkProTheme,
|
||||
shadesOfPurpleTheme,
|
||||
nightowlTheme,
|
||||
} from "./default-themes"
|
||||
|
|
|
|||
131
packages/ui/src/theme/themes/nightowl.json
Normal file
131
packages/ui/src/theme/themes/nightowl.json
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
{
|
||||
"$schema": "https://opencode.ai/desktop-theme.json",
|
||||
"name": "Night Owl",
|
||||
"id": "nightowl",
|
||||
"light": {
|
||||
"seeds": {
|
||||
"neutral": "#f0f0f0",
|
||||
"primary": "#4876d6",
|
||||
"success": "#2aa298",
|
||||
"warning": "#c96765",
|
||||
"error": "#de3d3b",
|
||||
"info": "#4876d6",
|
||||
"interactive": "#4876d6",
|
||||
"diffAdd": "#2aa298",
|
||||
"diffDelete": "#de3d3b"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#fbfbfb",
|
||||
"background-weak": "#f0f0f0",
|
||||
"background-strong": "#ffffff",
|
||||
"background-stronger": "#ffffff",
|
||||
"border-weak-base": "#d9d9d9",
|
||||
"border-weak-hover": "#cccccc",
|
||||
"border-weak-active": "#bfbfbf",
|
||||
"border-weak-selected": "#4876d6",
|
||||
"border-weak-disabled": "#e6e6e6",
|
||||
"border-weak-focus": "#4876d6",
|
||||
"border-base": "#c0c0c0",
|
||||
"border-hover": "#b3b3b3",
|
||||
"border-active": "#a6a6a6",
|
||||
"border-selected": "#4876d6",
|
||||
"border-disabled": "#d9d9d9",
|
||||
"border-focus": "#4876d6",
|
||||
"border-strong-base": "#90a7b2",
|
||||
"border-strong-hover": "#7d9aa6",
|
||||
"border-strong-active": "#6a8d9a",
|
||||
"border-strong-selected": "#4876d6",
|
||||
"border-strong-disabled": "#c0c0c0",
|
||||
"border-strong-focus": "#4876d6",
|
||||
"surface-diff-add-base": "#eaf8f6",
|
||||
"surface-diff-delete-base": "#fbe9e9",
|
||||
"surface-diff-hidden-base": "#e8f0fc",
|
||||
"text-base": "#403f53",
|
||||
"text-weak": "#7a8181",
|
||||
"text-strong": "#1a1a1a",
|
||||
"syntax-string": "#c96765",
|
||||
"syntax-primitive": "#aa0982",
|
||||
"syntax-property": "#4876d6",
|
||||
"syntax-type": "#994cc3",
|
||||
"syntax-constant": "#2aa298",
|
||||
"syntax-info": "#4876d6",
|
||||
"markdown-heading": "#4876d6",
|
||||
"markdown-text": "#403f53",
|
||||
"markdown-link": "#4876d6",
|
||||
"markdown-link-text": "#2aa298",
|
||||
"markdown-code": "#2aa298",
|
||||
"markdown-block-quote": "#7a8181",
|
||||
"markdown-emph": "#994cc3",
|
||||
"markdown-strong": "#c96765",
|
||||
"markdown-horizontal-rule": "#90a7b2",
|
||||
"markdown-list-item": "#4876d6",
|
||||
"markdown-list-enumeration": "#2aa298",
|
||||
"markdown-image": "#4876d6",
|
||||
"markdown-image-text": "#2aa298",
|
||||
"markdown-code-block": "#403f53"
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"seeds": {
|
||||
"neutral": "#011627",
|
||||
"primary": "#82aaff",
|
||||
"success": "#c5e478",
|
||||
"warning": "#ecc48d",
|
||||
"error": "#ef5350",
|
||||
"info": "#82aaff",
|
||||
"interactive": "#82aaff",
|
||||
"diffAdd": "#c5e478",
|
||||
"diffDelete": "#ef5350"
|
||||
},
|
||||
"overrides": {
|
||||
"background-base": "#011627",
|
||||
"background-weak": "#0b253a",
|
||||
"background-strong": "#001122",
|
||||
"background-stronger": "#000c17",
|
||||
"border-weak-base": "#1d3b53",
|
||||
"border-weak-hover": "#234561",
|
||||
"border-weak-active": "#2a506f",
|
||||
"border-weak-selected": "#82aaff",
|
||||
"border-weak-disabled": "#0f2132",
|
||||
"border-weak-focus": "#82aaff",
|
||||
"border-base": "#3a5a75",
|
||||
"border-hover": "#456785",
|
||||
"border-active": "#507494",
|
||||
"border-selected": "#82aaff",
|
||||
"border-disabled": "#1a3347",
|
||||
"border-focus": "#82aaff",
|
||||
"border-strong-base": "#5f7e97",
|
||||
"border-strong-hover": "#6e8da6",
|
||||
"border-strong-active": "#7d9cb5",
|
||||
"border-strong-selected": "#82aaff",
|
||||
"border-strong-disabled": "#2c4a63",
|
||||
"border-strong-focus": "#82aaff",
|
||||
"surface-diff-add-base": "#0a2e1a",
|
||||
"surface-diff-delete-base": "#2d1b1b",
|
||||
"surface-diff-hidden-base": "#0b253a",
|
||||
"text-base": "#d6deeb",
|
||||
"text-weak": "#5f7e97",
|
||||
"text-strong": "#ffffff",
|
||||
"syntax-string": "#ecc48d",
|
||||
"syntax-primitive": "#f78c6c",
|
||||
"syntax-property": "#82aaff",
|
||||
"syntax-type": "#c5e478",
|
||||
"syntax-constant": "#7fdbca",
|
||||
"syntax-info": "#82aaff",
|
||||
"markdown-heading": "#82aaff",
|
||||
"markdown-text": "#d6deeb",
|
||||
"markdown-link": "#82aaff",
|
||||
"markdown-link-text": "#7fdbca",
|
||||
"markdown-code": "#c5e478",
|
||||
"markdown-block-quote": "#5f7e97",
|
||||
"markdown-emph": "#c792ea",
|
||||
"markdown-strong": "#ecc48d",
|
||||
"markdown-horizontal-rule": "#5f7e97",
|
||||
"markdown-list-item": "#82aaff",
|
||||
"markdown-list-enumeration": "#7fdbca",
|
||||
"markdown-image": "#82aaff",
|
||||
"markdown-image-text": "#7fdbca",
|
||||
"markdown-code-block": "#d6deeb"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@opencode-ai/util",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@opencode-ai/web",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.6",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
|
||||
|
|
|
|||
|
|
@ -32,21 +32,74 @@ different order of precedence.
|
|||
Configuration files are **merged together**, not replaced.
|
||||
:::
|
||||
|
||||
Configuration files are merged together, not replaced. Settings from the following config locations are combined. Where later configs override earlier ones only for conflicting keys. Non-conflicting settings from all configs are preserved.
|
||||
Configuration files are merged together, not replaced. Settings from the following config locations are combined. Later configs override earlier ones only for conflicting keys. Non-conflicting settings from all configs are preserved.
|
||||
|
||||
For example, if your global config sets `theme: "opencode"` and `autoupdate: true`, and your project config sets `model: "anthropic/claude-sonnet-4-5"`, the final configuration will include all three settings.
|
||||
|
||||
---
|
||||
|
||||
### Precedence order
|
||||
|
||||
Config sources are loaded in this order (later sources override earlier ones):
|
||||
|
||||
1. **Remote config** (from `.well-known/opencode`) - organizational defaults
|
||||
2. **Global config** (`~/.config/opencode/opencode.json`) - user preferences
|
||||
3. **Custom config** (`OPENCODE_CONFIG` env var) - custom overrides
|
||||
4. **Project config** (`opencode.json` in project) - project-specific settings
|
||||
5. **`.opencode` directories** - agents, commands, plugins
|
||||
6. **Inline config** (`OPENCODE_CONFIG_CONTENT` env var) - runtime overrides
|
||||
|
||||
This means project configs can override global defaults, and global configs can override remote organizational defaults.
|
||||
|
||||
---
|
||||
|
||||
### Remote
|
||||
|
||||
Organizations can provide default configuration via the `.well-known/opencode` endpoint. This is fetched automatically when you authenticate with a provider that supports it.
|
||||
|
||||
Remote config is loaded first, serving as the base layer. All other config sources (global, project) can override these defaults.
|
||||
|
||||
For example, if your organization provides MCP servers that are disabled by default:
|
||||
|
||||
```json title="Remote config from .well-known/opencode"
|
||||
{
|
||||
"mcp": {
|
||||
"jira": {
|
||||
"type": "remote",
|
||||
"url": "https://jira.example.com/mcp",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can enable specific servers in your local config:
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"mcp": {
|
||||
"jira": {
|
||||
"type": "remote",
|
||||
"url": "https://jira.example.com/mcp",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Global
|
||||
|
||||
Place your global OpenCode config in `~/.config/opencode/opencode.json`. You'll want to use the global config for things like themes, providers, or keybinds.
|
||||
Place your global OpenCode config in `~/.config/opencode/opencode.json`. Use global config for user-wide preferences like themes, providers, or keybinds.
|
||||
|
||||
Global config overrides remote organizational defaults.
|
||||
|
||||
---
|
||||
|
||||
### Per project
|
||||
|
||||
You can also add a `opencode.json` in your project. Settings from this config are merged with and can override the global config. This is useful for configuring providers or modes specific to your project.
|
||||
Add `opencode.json` in your project root. Project config has the highest precedence among standard config files - it overrides both global and remote configs.
|
||||
|
||||
:::tip
|
||||
Place project specific config in the root of your project.
|
||||
|
|
@ -60,20 +113,20 @@ This is also safe to be checked into Git and uses the same schema as the global
|
|||
|
||||
### Custom path
|
||||
|
||||
You can also specify a custom config file path using the `OPENCODE_CONFIG` environment variable.
|
||||
Specify a custom config file path using the `OPENCODE_CONFIG` environment variable.
|
||||
|
||||
```bash
|
||||
export OPENCODE_CONFIG=/path/to/my/custom-config.json
|
||||
opencode run "Hello world"
|
||||
```
|
||||
|
||||
Settings from this config are merged with and **can override** the global and project configs.
|
||||
Custom config is loaded between global and project configs in the precedence order.
|
||||
|
||||
---
|
||||
|
||||
### Custom directory
|
||||
|
||||
You can specify a custom config directory using the `OPENCODE_CONFIG_DIR`
|
||||
Specify a custom config directory using the `OPENCODE_CONFIG_DIR`
|
||||
environment variable. This directory will be searched for agents, commands,
|
||||
modes, and plugins just like the standard `.opencode` directory, and should
|
||||
follow the same structure.
|
||||
|
|
|
|||
|
|
@ -76,9 +76,11 @@ You can also install it with the following commands:
|
|||
- **Using Homebrew on macOS and Linux**
|
||||
|
||||
```bash
|
||||
brew install opencode
|
||||
brew install anomalyco/tap/opencode
|
||||
```
|
||||
|
||||
> We recommend using the OpenCode tap for the most up to date releases. The official `brew install opencode` formula is maintained by the Homebrew team and is updated less frequently.
|
||||
|
||||
- **Using Paru on Arch Linux**
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -44,6 +44,29 @@ You can also disable a server by setting `enabled` to `false`. This is useful if
|
|||
|
||||
---
|
||||
|
||||
### Overriding remote defaults
|
||||
|
||||
Organizations can provide default MCP servers via their `.well-known/opencode` endpoint. These servers may be disabled by default, allowing users to opt-in to the ones they need.
|
||||
|
||||
To enable a specific server from your organization's remote config, add it to your local config with `enabled: true`:
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"jira": {
|
||||
"type": "remote",
|
||||
"url": "https://jira.example.com/mcp",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Your local config values override the remote defaults. See [config precedence](/docs/config#precedence-order) for more details.
|
||||
|
||||
---
|
||||
|
||||
## Local
|
||||
|
||||
Add local MCP servers using `type` to `"local"` within the MCP object.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue