Browse Source

fix: replace collapsible agents with compactSidebar opt-in layout

- Removes collapsible arrow toggle (▼/▶) from agents header
- Adds compactSidebar config option (default false) for single-line agents
- Compact format: label  provider/model  (variant)
- Label gets fixed 14-char width for column alignment
- Variant shown in parentheses for visual separation
- Removes the agents dropdown test (tested the collapsible behavior)
- Keeps default multi-line layout unchanged
Michael Henke 1 month ago
parent
commit
83d4d832d8
2 changed files with 30 additions and 195 deletions
  1. 5 130
      src/tui.test.ts
  2. 25 65
      src/tui.ts

+ 5 - 130
src/tui.test.ts

@@ -1,31 +1,14 @@
-import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
+import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
 import * as fs from 'node:fs';
 import * as os from 'node:os';
 import * as path from 'node:path';
-import type { TuiSnapshot } from './tui-state';
-
-type TestNode = {
-  tag: string;
-  props: Record<string, unknown>;
-  children: Array<TestNode | string | number>;
-};
-
-mock.module('@opentui/solid', () => ({
-  createElement: (tag: string): TestNode => ({ tag, props: {}, children: [] }),
-  insert: (node: TestNode, child: TestNode | string | number) => {
-    node.children.push(child);
-  },
-  setProp: (node: TestNode, key: string, value: unknown) => {
-    node.props[key] = value;
-  },
-}));
-
-const {
+import {
   getSidebarAgentNames,
   readConfigInvalid,
   splitSidebarModelId,
-  default: tuiPlugin,
-} = await import('./tui');
+  default as tuiPlugin,
+} from './tui';
+import type { TuiSnapshot } from './tui-state';
 
 function createSnapshot(overrides: Partial<TuiSnapshot> = {}): TuiSnapshot {
   return {
@@ -138,115 +121,7 @@ describe('readConfigInvalid', () => {
   });
 });
 
-function nodeText(node: TestNode | string | number): string {
-  if (typeof node !== 'object') return String(node);
-  return `${node.props.content ?? ''}${node.children.map(nodeText).join('')}`;
-}
-
-function findNode(
-  node: TestNode,
-  predicate: (node: TestNode) => boolean,
-): TestNode | undefined {
-  if (predicate(node)) return node;
-
-  for (const child of node.children) {
-    if (typeof child !== 'object') continue;
-    const match = findNode(child, predicate);
-    if (match) return match;
-  }
-}
-
-describe('tui agents dropdown', () => {
-  let originalEnv: typeof process.env;
-  let tempDir: string;
-
-  beforeEach(() => {
-    originalEnv = { ...process.env };
-    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'omos-tui-dropdown-'));
-    process.env.XDG_DATA_HOME = path.join(tempDir, 'data');
-    process.env.XDG_CONFIG_HOME = path.join(tempDir, 'config');
-
-    const stateFile = path.join(
-      process.env.XDG_DATA_HOME,
-      'opencode/storage/oh-my-opencode-slim/tui-state.json',
-    );
-    fs.mkdirSync(path.dirname(stateFile), { recursive: true });
-    fs.writeFileSync(
-      stateFile,
-      JSON.stringify({
-        version: 1,
-        updatedAt: 0,
-        agentModels: { explorer: 'openai/gpt-5.5' },
-        agentVariants: {},
-      }),
-    );
-  });
-
-  afterEach(() => {
-    fs.rmSync(tempDir, { recursive: true, force: true });
-    process.env = originalEnv;
-  });
-
-  test('clicking agents header hides and shows mounted agent rows', async () => {
-    let dispose = () => {};
-    let sidebarContent: (() => TestNode) | undefined;
 
-    await tuiPlugin.tui(
-      {
-        state: { path: { directory: tempDir } },
-        lifecycle: { onDispose: (fn: () => void) => (dispose = fn) },
-        renderer: {
-          requestRender: () => {},
-        },
-        slots: {
-          register: (registration: {
-            slots: { sidebar_content: () => TestNode };
-          }) => {
-            sidebarContent = registration.slots.sidebar_content;
-          },
-        },
-        theme: { current: {} },
-      } as unknown as Parameters<typeof tuiPlugin.tui>[0],
-      {},
-      { version: 'test' } as Parameters<typeof tuiPlugin.tui>[2],
-    );
-
-    const sidebar = sidebarContent?.();
-    if (!sidebar) throw new Error('sidebar content was not registered');
-
-    const header = findNode(
-      sidebar,
-      (node) => node.tag === 'text' && nodeText(node) === '▼ Agents',
-    );
-    const toggle = findNode(
-      sidebar,
-      (node) => typeof node.props.onMouseDown === 'function',
-    );
-    const row = findNode(
-      sidebar,
-      (node) =>
-        node.tag === 'box' &&
-        node.props.visible !== undefined &&
-        nodeText(node).includes('explorer'),
-    );
-    const onMouseDown = toggle?.props.onMouseDown;
-    if (!header || typeof onMouseDown !== 'function' || !row) {
-      throw new Error('agents dropdown nodes were not rendered');
-    }
-
-    expect(row.props.visible).toBe(true);
-
-    onMouseDown();
-    expect(header.props.content).toBe('▶ Agents');
-    expect(row.props.visible).toBe(false);
-
-    onMouseDown();
-    expect(header.props.content).toBe('▼ Agents');
-    expect(row.props.visible).toBe(true);
-
-    dispose();
-  });
-});
 
 describe('tui plugin env disable', () => {
   let originalEnv: typeof process.env;

+ 25 - 65
src/tui.ts

@@ -104,12 +104,19 @@ function agentRow(
   const modelParts = splitSidebarModelId(model);
   const detailRows: JSX.Element[] = [];
 
+  function detailRow(fieldLabel: string, value: string) {
+    return box({ width: '100%', flexDirection: 'row', paddingLeft: 2 }, [
+      text({ fg: theme.textMuted, width: 9 }, [fieldLabel]),
+      text({ fg: theme.textMuted }, [value]),
+    ]);
+  }
+
   if (modelParts.provider) {
-    detailRows.push(agentDetailRow('provider', modelParts.provider, theme));
+    detailRows.push(detailRow('provider', modelParts.provider));
   }
-  detailRows.push(agentDetailRow('model', modelParts.model, theme));
+  detailRows.push(detailRow('model', modelParts.model));
   if (variant) {
-    detailRows.push(agentDetailRow('variant', variant, theme));
+    detailRows.push(detailRow('variant', variant));
   }
 
   return box({ width: '100%', flexDirection: 'column', marginBottom: 1 }, [
@@ -124,32 +131,20 @@ function compactAgentRow(
   variant: string | undefined,
   theme: { textMuted: unknown },
 ): JSX.Element {
-  const value = variant ? `${model}  ${variant}` : model;
+  const value = variant ? `${model} (${variant})` : model;
   return box(
     {
       width: '100%',
       flexDirection: 'row',
       justifyContent: 'space-between',
-      marginBottom: 0,
     },
     [
-      text({ fg: theme.textMuted }, [label]),
+      text({ fg: theme.textMuted, width: 14 }, [label]),
       text({ fg: theme.textMuted }, [truncate(value, 40)]),
     ],
   );
 }
 
-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(
   snapshot: TuiSnapshot,
   version: string,
@@ -162,30 +157,8 @@ function renderSidebar(
   },
   configInvalid: boolean,
   compactSidebar: boolean,
-  agentsExpanded: boolean,
-  setAgentsExpanded: (expanded: boolean) => void,
 ): JSX.Element {
   const configStatusRow = buildConfigStatusRow(configInvalid, theme);
-  const agentNames = getSidebarAgentNames(snapshot);
-  const hasAgents = agentNames.length > 0;
-  const formatAgentsHeader = (expanded: boolean) =>
-    `${hasAgents ? (expanded ? '▼ ' : '▶ ') : ''}Agents`;
-  let expanded = agentsExpanded;
-  const agentRows = agentNames.map((agentName) => {
-    const model = snapshot.agentModels[agentName] ?? 'pending';
-    const variant = snapshot.agentVariants[agentName];
-    const row = compactSidebar
-      ? compactAgentRow(agentName, model, variant, theme)
-      : agentRow(agentName, model, variant, theme);
-
-    setProp(row, 'visible', expanded);
-    return row;
-  });
-  const agentsHeader = text(
-    { fg: theme.text, content: formatAgentsHeader(expanded) },
-    [],
-  );
-
   return box(
     {
       width: '100%',
@@ -214,25 +187,17 @@ function renderSidebar(
         ],
       ),
       configStatusRow,
-      box(
-        {
-          width: '100%',
-          flexDirection: 'row',
-          marginTop: 1,
-          onMouseDown: hasAgents
-            ? () => {
-                expanded = !expanded;
-                setAgentsExpanded(expanded);
-                setProp(agentsHeader, 'content', formatAgentsHeader(expanded));
-                for (const row of agentRows) {
-                  setProp(row, 'visible', expanded);
-                }
-              }
-            : undefined,
-        },
-        [agentsHeader],
-      ),
-      ...agentRows,
+      box({ width: '100%', marginTop: 1 }, [
+        text({ fg: theme.text }, ['Agents']),
+      ]),
+      ...getSidebarAgentNames(snapshot).map((agentName) => {
+        const model = snapshot.agentModels[agentName] ?? 'pending';
+        const variant = snapshot.agentVariants[agentName];
+        if (compactSidebar) {
+          return compactAgentRow(agentName, model, variant, theme);
+        }
+        return agentRow(agentName, model, variant, theme);
+      }),
     ],
   );
 }
@@ -268,7 +233,8 @@ function readConfigState(directory: string): {
       configInvalid = true;
     },
   });
-  return { configInvalid, compactSidebar: config.compactSidebar ?? false };
+  const compactSidebar = config.compactSidebar ?? false;
+  return { configInvalid, compactSidebar };
 }
 
 export function readConfigInvalid(directory: string): boolean {
@@ -283,7 +249,6 @@ const plugin: TuiPluginModule & { id: string } = {
     const version = meta.version ?? (await readPackageVersion()) ?? 'dev';
     let configDirectory = getTuiDirectory(api);
     let { configInvalid, compactSidebar } = readConfigState(configDirectory);
-    let agentsExpanded = true;
     let snapshot = readTuiSnapshot();
     const renderTimer = setInterval(async () => {
       try {
@@ -314,11 +279,6 @@ const plugin: TuiPluginModule & { id: string } = {
             api.theme.current,
             configInvalid,
             compactSidebar,
-            agentsExpanded,
-            (expanded) => {
-              agentsExpanded = expanded;
-              api.renderer.requestRender();
-            },
           );
         },
       },