Browse Source

fix: preserve plugin agent models when merging config

Alvin Unreal 3 weeks ago
parent
commit
bf9b29b793
2 changed files with 101 additions and 6 deletions
  1. 60 0
      src/index.test.ts
  2. 41 6
      src/index.ts

+ 60 - 0
src/index.test.ts

@@ -0,0 +1,60 @@
+import { describe, expect, test } from 'bun:test';
+import { mergePluginAgentConfig } from './index';
+
+describe('mergePluginAgentConfig', () => {
+  test('keeps plugin-managed model fields authoritative', () => {
+    const merged = mergePluginAgentConfig(
+      {
+        model: 'local/qwen3.5-27b-64k',
+        temperature: 0.25,
+        mode: 'subagent',
+      },
+      {
+        model: 'openai/gpt-5.4-mini',
+        temperature: 0.1,
+        mode: 'subagent',
+      },
+    );
+
+    expect(merged.model).toBe('local/qwen3.5-27b-64k');
+    expect(merged.temperature).toBe(0.25);
+  });
+
+  test('preserves user tools and merges permissions', () => {
+    const merged = mergePluginAgentConfig(
+      {
+        model: 'local/qwen3.5-27b-64k',
+        permission: {
+          question: 'allow',
+          skill: { '*': 'deny' },
+        },
+      },
+      {
+        tools: { bash: true },
+        permission: {
+          question: 'deny',
+          grep: 'allow',
+        },
+      },
+    );
+
+    expect(merged.tools).toEqual({ bash: true });
+    expect(merged.permission).toEqual({
+      question: 'deny',
+      skill: { '*': 'deny' },
+      grep: 'allow',
+    });
+  });
+
+  test('returns plugin config unchanged when no existing agent config', () => {
+    const merged = mergePluginAgentConfig({
+      model: 'local/qwen3.5-27b-64k',
+      prompt: 'test prompt',
+    });
+
+    expect(merged).toEqual({
+      model: 'local/qwen3.5-27b-64k',
+      prompt: 'test prompt',
+    });
+  });
+});

+ 41 - 6
src/index.ts

@@ -7,10 +7,10 @@ import {
   createAutoUpdateCheckerHook,
   createChatHeadersHook,
   createDelegateTaskRetryHook,
-  ForegroundFallbackManager,
   createJsonErrorRecoveryHook,
   createPhaseReminderHook,
   createPostReadNudgeHook,
+  ForegroundFallbackManager,
 } from './hooks';
 import { createBuiltinMcps } from './mcp';
 import {
@@ -27,6 +27,41 @@ import {
 import { startTmuxCheck } from './utils';
 import { log } from './utils/logger';
 
+function isRecord(value: unknown): value is Record<string, unknown> {
+  return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+export function mergePluginAgentConfig(
+  pluginAgent: Record<string, unknown>,
+  existing?: Record<string, unknown>,
+): Record<string, unknown> {
+  if (!existing) {
+    return { ...pluginAgent };
+  }
+
+  const merged: Record<string, unknown> = {
+    ...existing,
+    ...pluginAgent,
+  };
+
+  if ('tools' in existing) {
+    merged.tools = existing.tools;
+  }
+
+  if ('permission' in existing) {
+    if (isRecord(pluginAgent.permission) && isRecord(existing.permission)) {
+      merged.permission = {
+        ...pluginAgent.permission,
+        ...existing.permission,
+      };
+    } else {
+      merged.permission = existing.permission;
+    }
+  }
+
+  return merged;
+}
+
 const OhMyOpenCodeLite: Plugin = async (ctx) => {
   const config = loadPluginConfig(ctx.directory);
   const agentDefs = createAgents(config);
@@ -172,11 +207,11 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
             name
           ] as Record<string, unknown> | undefined;
           if (existing) {
-            // Shallow merge: plugin defaults first, user overrides win
-            (opencodeConfig.agent as Record<string, unknown>)[name] = {
-              ...pluginAgent,
-              ...existing,
-            };
+            (opencodeConfig.agent as Record<string, unknown>)[name] =
+              mergePluginAgentConfig(
+                pluginAgent as Record<string, unknown>,
+                existing,
+              );
           } else {
             (opencodeConfig.agent as Record<string, unknown>)[name] = {
               ...pluginAgent,