Browse Source

fix(tui): don't flag config invalid for deprecated-key warnings

Legacy fallback.* keys are stripped with a deprecation warning and the
config loads fine, but readConfigState set configInvalid on ANY loader
warning, so the TUI sidebar showed a persistent 'Config invalid' state.

Add a distinct 'deprecated-key' warning kind for benign deprecation
notices (fallback legacy keys, tmux, council.master) and only flag
configInvalid for genuine load failures: invalid-json, invalid-schema,
read-error. Missing-preset is also no longer treated as invalid.
adikpb 1 week ago
parent
commit
7e2cbe68b7
4 changed files with 48 additions and 11 deletions
  1. 5 5
      src/config/loader.test.ts
  2. 5 4
      src/config/loader.ts
  3. 26 0
      src/tui.test.ts
  4. 12 2
      src/tui.ts

+ 5 - 5
src/config/loader.test.ts

@@ -492,7 +492,7 @@ describe('onWarning callback', () => {
     expect(config.agents?.oracle?.model).toBe('valid/model');
   });
 
-  test('deprecated tmux key calls onWarning with invalid-schema and still loads', () => {
+  test('deprecated tmux key calls onWarning with deprecated-key and still loads', () => {
     const projectDir = path.join(tempDir, 'project');
     const projectConfigDir = path.join(projectDir, '.opencode');
     fs.mkdirSync(projectConfigDir, { recursive: true });
@@ -510,12 +510,12 @@ describe('onWarning callback', () => {
     });
 
     expect(warnings).toHaveLength(1);
-    expect(warnings[0]?.kind).toBe('invalid-schema');
+    expect(warnings[0]?.kind).toBe('deprecated-key');
     expect(warnings[0]?.message).toContain('Deprecated tmux config key');
     expect(config.agents?.oracle?.model).toBe('valid/model');
   });
 
-  test('deprecated council.master key calls onWarning with invalid-schema and still loads', () => {
+  test('deprecated council.master key calls onWarning with deprecated-key and still loads', () => {
     const projectDir = path.join(tempDir, 'project');
     const projectConfigDir = path.join(projectDir, '.opencode');
     fs.mkdirSync(projectConfigDir, { recursive: true });
@@ -539,7 +539,7 @@ describe('onWarning callback', () => {
     });
 
     expect(warnings).toHaveLength(1);
-    expect(warnings[0]?.kind).toBe('invalid-schema');
+    expect(warnings[0]?.kind).toBe('deprecated-key');
     expect(warnings[0]?.message).toContain(
       'Deprecated council.master config key',
     );
@@ -858,7 +858,7 @@ describe('deepMerge behavior', () => {
     });
 
     expect(warnings).toHaveLength(1);
-    expect(warnings[0]?.kind).toBe('invalid-schema');
+    expect(warnings[0]?.kind).toBe('deprecated-key');
     expect(warnings[0]?.message).toContain('Deprecated fallback config keys');
     expect(warnings[0]?.message).toContain('timeoutMs');
     expect(config.fallback?.enabled).toBe(true);

+ 5 - 4
src/config/loader.ts

@@ -17,7 +17,8 @@ export type ConfigLoadWarningKind =
   | 'invalid-json'
   | 'invalid-schema'
   | 'read-error'
-  | 'missing-preset';
+  | 'missing-preset'
+  | 'deprecated-key';
 
 /**
  * A warning emitted while loading plugin configuration.
@@ -98,7 +99,7 @@ function loadConfigFromPath(
         'Deprecated tmux config key found and ignored. Use multiplexer config instead.';
       options?.onWarning?.({
         path: configPath,
-        kind: 'invalid-schema' as ConfigLoadWarningKind,
+        kind: 'deprecated-key',
         message: tmuxMsg,
       });
       if (!options?.silent) {
@@ -122,7 +123,7 @@ function loadConfigFromPath(
         'Deprecated council.master config key found and ignored. Configure council agents via presets instead.';
       options?.onWarning?.({
         path: configPath,
-        kind: 'invalid-schema' as ConfigLoadWarningKind,
+        kind: 'deprecated-key',
         message: masterMsg,
       });
       if (!options?.silent) {
@@ -146,7 +147,7 @@ function loadConfigFromPath(
         const fallbackMsg = `Deprecated fallback config key${present.length === 1 ? '' : 's'} ${present.join(', ')} found and ignored. These fields were removed in 2.3.x; fallback behavior is controlled by fallback.enabled and fallback.maxRetries.`;
         options?.onWarning?.({
           path: configPath,
-          kind: 'invalid-schema' as ConfigLoadWarningKind,
+          kind: 'deprecated-key',
           message: fallbackMsg,
         });
         if (!options?.silent) {

+ 26 - 0
src/tui.test.ts

@@ -123,6 +123,32 @@ describe('readConfigInvalid', () => {
     }
   });
 
+  test('returns false for config with deprecated fallback keys (loads fine)', () => {
+    const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-tui-'));
+    try {
+      const projectDir = path.join(tempDir, 'project');
+      const configDir = path.join(projectDir, '.opencode');
+      fs.mkdirSync(configDir, { recursive: true });
+      fs.writeFileSync(
+        path.join(configDir, 'oh-my-opencode-slim.json'),
+        JSON.stringify({
+          fallback: {
+            enabled: true,
+            timeoutMs: 15000,
+            runtimeOverride: true,
+          },
+          agents: { oracle: { model: 'valid/model' } },
+        }),
+      );
+
+      // Deprecated fallback keys are stripped with a warning; the config
+      // loads successfully so the sidebar must NOT show "Config invalid".
+      expect(readConfigInvalid(projectDir)).toBe(false);
+    } finally {
+      fs.rmSync(tempDir, { recursive: true, force: true });
+    }
+  });
+
   test('uses compact sidebar by default', () => {
     const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-tui-'));
     try {

+ 12 - 2
src/tui.ts

@@ -297,8 +297,18 @@ function readConfigState(directory: string): {
   let configInvalid = false;
   const config = loadPluginConfig(directory, {
     silent: true,
-    onWarning: () => {
-      configInvalid = true;
+    onWarning: (warning) => {
+      // Only genuinely broken configs (parse/load/schema failures) mark the
+      // sidebar invalid. Benign deprecation notices (deprecated-key) and
+      // missing-preset do not, otherwise a config that loads fine would be
+      // shown as "Config invalid".
+      if (
+        warning.kind === 'invalid-json' ||
+        warning.kind === 'invalid-schema' ||
+        warning.kind === 'read-error'
+      ) {
+        configInvalid = true;
+      }
     },
   });
   const compactSidebar = config.compactSidebar ?? true;