Browse Source

fix(preset): support JSONC preset persistence

Knowingthesea_Qesire 2 months ago
parent
commit
57a7e64f3f
2 changed files with 60 additions and 2 deletions
  1. 54 0
      src/tools/preset-manager.test.ts
  2. 6 2
      src/tools/preset-manager.ts

+ 54 - 0
src/tools/preset-manager.test.ts

@@ -38,12 +38,15 @@ function getOutputText(output: ReturnType<typeof createOutput>): string {
 }
 
 let previousXdgDataHome: string | undefined;
+let previousOpenCodeConfigDir: string | undefined;
 let tempDir: string;
 
 beforeEach(() => {
   previousXdgDataHome = process.env.XDG_DATA_HOME;
+  previousOpenCodeConfigDir = process.env.OPENCODE_CONFIG_DIR;
   tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-preset-manager-'));
   process.env.XDG_DATA_HOME = tempDir;
+  delete process.env.OPENCODE_CONFIG_DIR;
   setActiveRuntimePreset(null);
 });
 
@@ -54,6 +57,12 @@ afterEach(() => {
     process.env.XDG_DATA_HOME = previousXdgDataHome;
   }
 
+  if (previousOpenCodeConfigDir === undefined) {
+    delete process.env.OPENCODE_CONFIG_DIR;
+  } else {
+    process.env.OPENCODE_CONFIG_DIR = previousOpenCodeConfigDir;
+  }
+
   fs.rmSync(tempDir, { recursive: true, force: true });
   setActiveRuntimePreset(null);
 });
@@ -198,6 +207,51 @@ describe('createPresetManager', () => {
       });
     });
 
+    test('persists preset changes from JSONC user config', async () => {
+      const configDir = path.join(tempDir, 'opencode-config');
+      fs.mkdirSync(configDir, { recursive: true });
+      process.env.OPENCODE_CONFIG_DIR = configDir;
+
+      const configPath = path.join(configDir, 'oh-my-opencode-slim.jsonc');
+      fs.writeFileSync(
+        configPath,
+        `{
+          // User-selected preset should be updated even in JSONC files.
+          "preset": "old",
+          "agents": {
+            "orchestrator": { "model": "old-model" },
+          },
+        }`,
+      );
+
+      const ctx = { ...createMockContext(), directory: tempDir };
+      const config: PluginConfig = {
+        presets: {
+          cheap: {
+            orchestrator: { model: 'anthropic/claude-3.5-haiku' },
+          },
+        },
+      };
+      const manager = createPresetManager(ctx, config);
+      const output = createOutput();
+
+      await manager.handleCommandExecuteBefore(
+        { command: 'preset', sessionID: 's1', arguments: 'cheap' },
+        output,
+      );
+
+      const persisted = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as {
+        preset?: string;
+        agents?: Record<string, unknown>;
+      };
+      expect(persisted.preset).toBe('cheap');
+      expect(persisted.agents).toEqual({
+        orchestrator: { model: 'old-model' },
+      });
+      expect(ctx.client.config.update).not.toHaveBeenCalled();
+      expect(ctx.client.instance.dispose).not.toHaveBeenCalled();
+    });
+
     test('shows temperature in preset summary without runtime config update', async () => {
       const ctx = createMockContext();
       const config: PluginConfig = {

+ 6 - 2
src/tools/preset-manager.ts

@@ -1,5 +1,6 @@
 import * as fs from 'node:fs';
 import type { PluginInput } from '@opencode-ai/plugin';
+import { stripJsonComments } from '../cli/config-io';
 import type {
   AgentOverrideConfig,
   ModelEntry,
@@ -162,7 +163,10 @@ export function createPresetManager(ctx: PluginInput, config: PluginConfig) {
       const { userConfigPath } = findPluginConfigPaths(ctx.directory);
       if (userConfigPath) {
         const raw = fs.readFileSync(userConfigPath, 'utf-8');
-        const persisted = JSON.parse(raw) as Record<string, unknown>;
+        const persisted = JSON.parse(stripJsonComments(raw)) as Record<
+          string,
+          unknown
+        >;
         persisted.preset = presetName;
         fs.writeFileSync(
           userConfigPath,
@@ -204,7 +208,7 @@ export function createPresetManager(ctx: PluginInput, config: PluginConfig) {
 
   /**
    * Map an AgentOverrideConfig (from plugin config) to the subset of
-   * SDK AgentConfig fields that client.config.update() can apply at runtime.
+   * Agent config fields shown in the saved preset summary.
    *
    * Excluded fields and why:
    * - prompt, orchestratorPrompt: require restart (resolved at init by config() hook)