Browse Source

Merge pull request #574 from fslse/feat/tui-agent-model-details

Show agent provider, model, and variant in TUI
Alvin 1 month ago
parent
commit
8428713b65
6 changed files with 122 additions and 33 deletions
  1. 14 1
      src/index.ts
  2. 7 1
      src/tools/preset-manager.ts
  3. 33 0
      src/tui-state.test.ts
  4. 9 0
      src/tui-state.ts
  5. 15 8
      src/tui.test.ts
  6. 44 23
      src/tui.ts

+ 14 - 1
src/index.ts

@@ -611,6 +611,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
       }
 
       const tuiAgentModels: Record<string, string> = {};
+      const tuiAgentVariants: Record<string, string> = {};
       for (const agentDef of agentDefs) {
         if (agentDef.name === 'councillor') continue;
 
@@ -625,10 +626,22 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
               : typeof agentDef.config.model === 'string'
                 ? agentDef.config.model
                 : undefined;
+        const resolvedVariant =
+          typeof entry?.variant === 'string'
+            ? entry.variant
+            : typeof agentDef.config.variant === 'string'
+              ? agentDef.config.variant
+              : undefined;
 
         tuiAgentModels[agentDef.name] = resolvedModel ?? 'default';
+        if (resolvedVariant) {
+          tuiAgentVariants[agentDef.name] = resolvedVariant;
+        }
       }
-      recordTuiAgentModels({ agentModels: tuiAgentModels });
+      recordTuiAgentModels({
+        agentModels: tuiAgentModels,
+        agentVariants: tuiAgentVariants,
+      });
 
       // Merge MCP configs
       const configMcp = opencodeConfig.mcp as

+ 7 - 1
src/tools/preset-manager.ts

@@ -179,13 +179,19 @@ export function createPresetManager(ctx: PluginInput, config: PluginConfig) {
 
     const snapshot = readTuiSnapshot();
     const agentModels = { ...snapshot.agentModels };
+    const agentVariants = { ...snapshot.agentVariants };
     for (const [agentName, agentConfig] of Object.entries(agentUpdates)) {
       if (typeof agentConfig.model === 'string') {
         agentModels[agentName] = agentConfig.model;
       }
+      if (typeof agentConfig.variant === 'string') {
+        agentVariants[agentName] = agentConfig.variant;
+      } else {
+        delete agentVariants[agentName];
+      }
     }
 
-    recordTuiAgentModels({ agentModels });
+    recordTuiAgentModels({ agentModels, agentVariants });
 
     activePreset = presetName;
 

+ 33 - 0
src/tui-state.test.ts

@@ -34,6 +34,10 @@ describe('tui-state persistence', () => {
         explorer: 'openai/gpt-5.4-mini',
         fixer: 'openai/gpt-5.4-mini',
       },
+      agentVariants: {
+        explorer: 'low',
+        fixer: 'high',
+      },
     });
 
     const snapshot = readTuiSnapshot();
@@ -42,6 +46,10 @@ describe('tui-state persistence', () => {
       explorer: 'openai/gpt-5.4-mini',
       fixer: 'openai/gpt-5.4-mini',
     });
+    expect(snapshot.agentVariants).toEqual({
+      explorer: 'low',
+      fixer: 'high',
+    });
   });
 
   test('updates a single live agent model without dropping others', () => {
@@ -62,6 +70,30 @@ describe('tui-state persistence', () => {
       explorer: 'openai/gpt-5.4-mini',
     });
   });
+
+  test('updates a single live agent variant without dropping others', () => {
+    recordTuiAgentModels({
+      agentModels: {
+        orchestrator: 'default',
+        explorer: 'openai/gpt-5.4-mini',
+      },
+      agentVariants: {
+        explorer: 'low',
+      },
+    });
+
+    recordTuiAgentModel({
+      agentName: 'orchestrator',
+      model: 'openai/gpt-5.5',
+      variant: 'high',
+    });
+
+    expect(readTuiSnapshot().agentVariants).toEqual({
+      orchestrator: 'high',
+      explorer: 'low',
+    });
+  });
+
   test('ignores legacy config status fields in old snapshots', () => {
     const filePath = path.join(
       tempDir,
@@ -86,5 +118,6 @@ describe('tui-state persistence', () => {
     expect(snapshot.agentModels).toEqual({
       explorer: 'openai/gpt-5.4-mini',
     });
+    expect(snapshot.agentVariants).toEqual({});
   });
 });

+ 9 - 0
src/tui-state.ts

@@ -6,6 +6,7 @@ export interface TuiSnapshot {
   version: 1;
   updatedAt: number;
   agentModels: Record<string, string>;
+  agentVariants: Record<string, string>;
 }
 
 const STATE_DIR = 'oh-my-opencode-slim';
@@ -26,6 +27,7 @@ function emptySnapshot(): TuiSnapshot {
     version: 1,
     updatedAt: Date.now(),
     agentModels: {},
+    agentVariants: {},
   };
 }
 
@@ -38,6 +40,7 @@ function parseSnapshot(value: string): TuiSnapshot {
     updatedAt:
       typeof parsed.updatedAt === 'number' ? parsed.updatedAt : Date.now(),
     agentModels: parsed.agentModels ?? {},
+    agentVariants: parsed.agentVariants ?? {},
   };
 }
 
@@ -76,17 +79,23 @@ function updateSnapshot(mutator: (snapshot: TuiSnapshot) => void): void {
 
 export function recordTuiAgentModels(input: {
   agentModels: Record<string, string>;
+  agentVariants?: Record<string, string>;
 }): void {
   updateSnapshot((snapshot) => {
     snapshot.agentModels = { ...input.agentModels };
+    snapshot.agentVariants = { ...(input.agentVariants ?? {}) };
   });
 }
 
 export function recordTuiAgentModel(input: {
   agentName: string;
   model: string;
+  variant?: string;
 }): void {
   updateSnapshot((snapshot) => {
     snapshot.agentModels[input.agentName] = input.model;
+    if (input.variant !== undefined) {
+      snapshot.agentVariants[input.agentName] = input.variant;
+    }
   });
 }

+ 15 - 8
src/tui.test.ts

@@ -3,9 +3,9 @@ import * as fs from 'node:fs';
 import * as os from 'node:os';
 import * as path from 'node:path';
 import {
-  formatSidebarModelName,
   getSidebarAgentNames,
   readConfigInvalid,
+  splitSidebarModelId,
 } from './tui';
 import type { TuiSnapshot } from './tui-state';
 
@@ -14,6 +14,7 @@ function createSnapshot(overrides: Partial<TuiSnapshot> = {}): TuiSnapshot {
     version: 1,
     updatedAt: 0,
     agentModels: {},
+    agentVariants: {},
     ...overrides,
   };
 }
@@ -45,18 +46,24 @@ describe('tui sidebar agents', () => {
   });
 });
 
-describe('formatSidebarModelName', () => {
-  test('keeps only the segment after the last slash', () => {
-    expect(formatSidebarModelName('openai/gpt-5.5-fast')).toBe('gpt-5.5-fast');
+describe('splitSidebarModelId', () => {
+  test('splits provider from model at the first slash', () => {
+    expect(splitSidebarModelId('openai/gpt-5.5-fast')).toEqual({
+      provider: 'openai',
+      model: 'gpt-5.5-fast',
+    });
     expect(
-      formatSidebarModelName(
+      splitSidebarModelId(
         'fireworks-ai/accounts/fireworks/routers/kimi-k2p5-turbo',
       ),
-    ).toBe('kimi-k2p5-turbo');
+    ).toEqual({
+      provider: 'fireworks-ai',
+      model: 'accounts/fireworks/routers/kimi-k2p5-turbo',
+    });
   });
 
-  test('leaves model names without slashes unchanged', () => {
-    expect(formatSidebarModelName('pending')).toBe('pending');
+  test('keeps slashless names as model only', () => {
+    expect(splitSidebarModelId('pending')).toEqual({ model: 'pending' });
   });
 });
 

+ 44 - 23
src/tui.ts

@@ -62,19 +62,25 @@ function box(props: Record<string, unknown>, children: Child[] = []) {
   return element('box', props, children);
 }
 
-function truncate(value: string, max = 24): string {
-  return value.length > max ? `${value.slice(0, max - 1)}…` : value;
-}
-
 function getTuiDirectory(api: {
   state?: { path?: { directory?: string } };
 }): string {
   return api.state?.path?.directory ?? process.cwd();
 }
 
-export function formatSidebarModelName(model: string): string {
-  const lastSlash = model.lastIndexOf('/');
-  return lastSlash === -1 ? model : model.slice(lastSlash + 1);
+export function splitSidebarModelId(model: string): {
+  provider?: string;
+  model: string;
+} {
+  const slashIndex = model.indexOf('/');
+  if (slashIndex === -1) {
+    return { model };
+  }
+
+  return {
+    provider: model.slice(0, slashIndex),
+    model: model.slice(slashIndex + 1),
+  };
 }
 
 export function getSidebarAgentNames(snapshot: TuiSnapshot): string[] {
@@ -84,19 +90,38 @@ export function getSidebarAgentNames(snapshot: TuiSnapshot): string[] {
     : FALLBACK_SIDEBAR_AGENTS;
 }
 
-function row(
+function agentRow(
   label: string,
-  value: string,
+  model: string,
+  variant: string | undefined,
   theme: { textMuted: unknown; text: unknown },
-  valueColor?: unknown,
 ): JSX.Element {
-  return box(
-    { width: '100%', flexDirection: 'row', justifyContent: 'space-between' },
-    [
-      text({ fg: theme.textMuted }, [label]),
-      text({ fg: valueColor ?? theme.text }, [value]),
-    ],
-  );
+  const modelParts = splitSidebarModelId(model);
+  const detailRows: JSX.Element[] = [];
+
+  if (modelParts.provider) {
+    detailRows.push(agentDetailRow('provider', modelParts.provider, theme));
+  }
+  detailRows.push(agentDetailRow('model', modelParts.model, theme));
+  if (variant) {
+    detailRows.push(agentDetailRow('variant', variant, theme));
+  }
+
+  return box({ width: '100%', flexDirection: 'column', marginBottom: 1 }, [
+    text({ fg: theme.textMuted }, [label]),
+    ...detailRows,
+  ]);
+}
+
+function agentDetailRow(
+  label: string,
+  value: string,
+  theme: { textMuted: unknown },
+): JSX.Element {
+  return box({ width: '100%', flexDirection: 'row', paddingLeft: 2 }, [
+    text({ fg: theme.textMuted, width: 9 }, [label]),
+    text({ fg: theme.textMuted }, [value]),
+  ]);
 }
 
 function renderSidebar(
@@ -146,12 +171,8 @@ function renderSidebar(
       ]),
       ...getSidebarAgentNames(snapshot).map((agentName) => {
         const model = snapshot.agentModels[agentName] ?? 'pending';
-        return row(
-          agentName,
-          truncate(formatSidebarModelName(model), 26),
-          theme,
-          theme.textMuted,
-        );
+        const variant = snapshot.agentVariants[agentName];
+        return agentRow(agentName, model, variant, theme);
       }),
     ],
   );