From 643cca54afe393fb2787f0e8fa7ed5d09c6fd367 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Mon, 24 Nov 2025 22:02:19 -0500 Subject: [PATCH 1/8] tui: align session panel content with consistent left padding --- .opencode/command/commit.md | 1 - packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index 9626f172cf9..2e3d759b654 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -1,6 +1,5 @@ --- description: Git commit and push -subtask: true --- commit and push diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index dc3d17a8abb..6a23e97ccc9 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -939,7 +939,7 @@ function UserMessage(props: { onMouseUp={props.onMouseUp} paddingTop={1} paddingBottom={1} - paddingLeft={1} + paddingLeft={2} backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel} flexShrink={0} > From 88a02428721da5c990d4ca75c2d8bfb34725d621 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Tue, 25 Nov 2025 01:33:37 -0500 Subject: [PATCH 2/8] fix: merge plugin selections from local and global configurations and plugins folder --- packages/opencode/src/config/config.ts | 20 ++++++-- packages/opencode/test/config/config.test.ts | 48 ++++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 779a4e8e2a3..3fa0d18bb72 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -21,25 +21,35 @@ import { ConfigMarkdown } from "./markdown" export namespace Config { const log = Log.create({ service: "config" }) + // Custom merge function that concatenates plugin arrays instead of replacing them + function mergeConfigWithPlugins(target: Info, source: Info): Info { + const merged = mergeDeep(target, source) + // If both configs have plugin arrays, concatenate them instead of replacing + if (target.plugin && source.plugin) { + merged.plugin = [...target.plugin, ...source.plugin] + } + return merged + } + export const state = Instance.state(async () => { const auth = await Auth.all() let result = await global() // Override with custom config if provided if (Flag.OPENCODE_CONFIG) { - result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG)) + result = mergeConfigWithPlugins(result, await loadFile(Flag.OPENCODE_CONFIG)) log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG }) } for (const file of ["opencode.jsonc", "opencode.json"]) { const found = await Filesystem.findUp(file, Instance.directory, Instance.worktree) for (const resolved of found.toReversed()) { - result = mergeDeep(result, await loadFile(resolved)) + result = mergeConfigWithPlugins(result, await loadFile(resolved)) } } if (Flag.OPENCODE_CONFIG_CONTENT) { - result = mergeDeep(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT)) + result = mergeConfigWithPlugins(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT)) log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT") } @@ -47,7 +57,7 @@ export namespace Config { 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 = mergeDeep(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd())) + result = mergeConfigWithPlugins(result, await load(JSON.stringify(wellknown.config ?? {}), process.cwd())) } } @@ -78,7 +88,7 @@ export namespace Config { if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) { for (const file of ["opencode.jsonc", "opencode.json"]) { log.debug(`loading config from ${path.join(dir, file)}`) - result = mergeDeep(result, await loadFile(path.join(dir, file))) + result = mergeConfigWithPlugins(result, await loadFile(path.join(dir, file))) // to satisy the type checker result.agent ??= {} result.mode ??= {} diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 967972842f5..ed9f1087362 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -403,3 +403,51 @@ test("resolves scoped npm plugins in config", async () => { }, }) }) + +test("merges plugin arrays from global and local configs", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + // Create a nested project structure with local .opencode config + const projectDir = path.join(dir, "project") + const opencodeDir = path.join(projectDir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + // Global config with plugins + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + plugin: ["global-plugin-1", "global-plugin-2"], + }), + ) + + // Local .opencode config with different plugins + await Bun.write( + path.join(opencodeDir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + plugin: ["local-plugin-1"], + }), + ) + }, + }) + + await Instance.provide({ + directory: path.join(tmp.path, "project"), + fn: async () => { + const config = await Config.get() + const plugins = config.plugin ?? [] + + // Should contain both global and local plugins + expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true) + expect(plugins.some((p) => p.includes("global-plugin-2"))).toBe(true) + expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true) + + // Should have all 3 plugins (not replaced, but merged) + const pluginNames = plugins.filter( + (p) => p.includes("global-plugin") || p.includes("local-plugin"), + ) + expect(pluginNames.length).toBeGreaterThanOrEqual(3) + }, + }) +}) From 412716249caee1949579ee28684f32c89a0525d2 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 25 Nov 2025 06:43:19 +0000 Subject: [PATCH 3/8] chore: format code --- packages/opencode/test/config/config.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index ed9f1087362..1764cbf14ec 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -444,9 +444,7 @@ test("merges plugin arrays from global and local configs", async () => { expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true) // Should have all 3 plugins (not replaced, but merged) - const pluginNames = plugins.filter( - (p) => p.includes("global-plugin") || p.includes("local-plugin"), - ) + const pluginNames = plugins.filter((p) => p.includes("global-plugin") || p.includes("local-plugin")) expect(pluginNames.length).toBeGreaterThanOrEqual(3) }, }) From 604688e2b61b05d3c3da12b900745e7fbcc62336 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Tue, 25 Nov 2025 02:24:33 -0500 Subject: [PATCH 4/8] ... --- packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 6a23e97ccc9..f8526e72be8 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -124,7 +124,8 @@ export function Session() { if (tui?.scroll_speed) { return new CustomSpeedScroll(tui.scroll_speed) } - return undefined + + return new CustomSpeedScroll(process.platform === "win32" ? 3 : 1) }) createEffect(async () => { From f96a0c7230e7d1f73a494f5d4be9823399cf22d0 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Tue, 25 Nov 2025 02:25:54 -0500 Subject: [PATCH 5/8] ... --- .opencode/command/commit.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.opencode/command/commit.md b/.opencode/command/commit.md index 2e3d759b654..9626f172cf9 100644 --- a/.opencode/command/commit.md +++ b/.opencode/command/commit.md @@ -1,5 +1,6 @@ --- description: Git commit and push +subtask: true --- commit and push From 497c5971ce5e342973a6da074ab99642aee3d2f5 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Tue, 25 Nov 2025 02:29:29 -0500 Subject: [PATCH 6/8] ... --- packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index f8526e72be8..105a67c7286 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -124,8 +124,7 @@ export function Session() { if (tui?.scroll_speed) { return new CustomSpeedScroll(tui.scroll_speed) } - - return new CustomSpeedScroll(process.platform === "win32" ? 3 : 1) + return undefined; }) createEffect(async () => { @@ -940,7 +939,7 @@ function UserMessage(props: { onMouseUp={props.onMouseUp} paddingTop={1} paddingBottom={1} - paddingLeft={2} + paddingLeft={1} backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel} flexShrink={0} > From 9028f30f3e3ba35d595058e1dc293b6d7af0e9dc Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Tue, 25 Nov 2025 02:30:04 -0500 Subject: [PATCH 7/8] ... --- packages/opencode/src/cli/cmd/tui/routes/session/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 105a67c7286..dc3d17a8abb 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -124,7 +124,7 @@ export function Session() { if (tui?.scroll_speed) { return new CustomSpeedScroll(tui.scroll_speed) } - return undefined; + return undefined }) createEffect(async () => { From c6f9a71c0a1fe80567ccefc1d0a9813f047e136b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Tue, 25 Nov 2025 17:32:54 +0000 Subject: [PATCH 8/8] Plugin merging now uses Set for deduplication Co-authored-by: rekram1-node --- packages/opencode/src/config/config.ts | 3 +- packages/opencode/test/config/config.test.ts | 52 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 3fa0d18bb72..aa47287d359 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -26,7 +26,8 @@ export namespace Config { const merged = mergeDeep(target, source) // If both configs have plugin arrays, concatenate them instead of replacing if (target.plugin && source.plugin) { - merged.plugin = [...target.plugin, ...source.plugin] + const pluginSet = new Set([...target.plugin, ...source.plugin]) + merged.plugin = Array.from(pluginSet) } return merged } diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 1764cbf14ec..2ff8c01cdb0 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -449,3 +449,55 @@ test("merges plugin arrays from global and local configs", async () => { }, }) }) + +test("deduplicates duplicate plugins from global and local configs", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + // Create a nested project structure with local .opencode config + const projectDir = path.join(dir, "project") + const opencodeDir = path.join(projectDir, ".opencode") + await fs.mkdir(opencodeDir, { recursive: true }) + + // Global config with plugins + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + plugin: ["duplicate-plugin", "global-plugin-1"], + }), + ) + + // Local .opencode config with some overlapping plugins + await Bun.write( + path.join(opencodeDir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + plugin: ["duplicate-plugin", "local-plugin-1"], + }), + ) + }, + }) + + await Instance.provide({ + directory: path.join(tmp.path, "project"), + fn: async () => { + const config = await Config.get() + const plugins = config.plugin ?? [] + + // Should contain all unique plugins + expect(plugins.some((p) => p.includes("global-plugin-1"))).toBe(true) + expect(plugins.some((p) => p.includes("local-plugin-1"))).toBe(true) + expect(plugins.some((p) => p.includes("duplicate-plugin"))).toBe(true) + + // Should deduplicate the duplicate plugin + const duplicatePlugins = plugins.filter((p) => p.includes("duplicate-plugin")) + expect(duplicatePlugins.length).toBe(1) + + // Should have exactly 3 unique plugins + const pluginNames = plugins.filter( + (p) => p.includes("global-plugin") || p.includes("local-plugin") || p.includes("duplicate-plugin"), + ) + expect(pluginNames.length).toBe(3) + }, + }) +})