Browse Source

feat: add compactSidebar option for single-line agent rows in TUI sidebar

Adds a new `compactSidebar` config option (default false) that renders
each agent on a single line in the TUI sidebar instead of the default
multi-line layout with indented provider/model/variant rows.

Closes #590
Michael Henke 1 month ago
parent
commit
60e3a84975
3 changed files with 53 additions and 5 deletions
  1. 4 0
      oh-my-opencode-slim.schema.json
  2. 6 0
      src/config/schema.ts
  3. 43 5
      src/tui.ts

+ 4 - 0
oh-my-opencode-slim.schema.json

@@ -12,6 +12,10 @@
       "description": "Disable automatic installation of plugin updates when false. Defaults to true.",
       "type": "boolean"
     },
+    "compactSidebar": {
+      "description": "Render each agent on a single line in the TUI sidebar instead of the default multi-line layout. Default false.",
+      "type": "boolean"
+    },
     "presets": {
       "type": "object",
       "propertyNames": {

+ 6 - 0
src/config/schema.ts

@@ -299,6 +299,12 @@ export const PluginConfigSchema = z
       .describe(
         'Disable automatic installation of plugin updates when false. Defaults to true.',
       ),
+    compactSidebar: z
+      .boolean()
+      .optional()
+      .describe(
+        'Render each agent on a single line in the TUI sidebar instead of the default multi-line layout. Default false.',
+      ),
     presets: z.record(z.string(), PresetSchema).optional(),
     agents: z.record(z.string(), AgentOverrideConfigSchema).optional(),
     disabled_agents: z

+ 43 - 5
src/tui.ts

@@ -91,6 +91,10 @@ export function getSidebarAgentNames(snapshot: TuiSnapshot): string[] {
     : FALLBACK_SIDEBAR_AGENTS;
 }
 
+function truncate(str: string, maxLen: number): string {
+  return str.length > maxLen ? `${str.slice(0, maxLen - 1)}…` : str;
+}
+
 function agentRow(
   label: string,
   model: string,
@@ -114,6 +118,27 @@ function agentRow(
   ]);
 }
 
+function compactAgentRow(
+  label: string,
+  model: string,
+  variant: string | undefined,
+  theme: { textMuted: unknown },
+): JSX.Element {
+  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 }, [truncate(value, 40)]),
+    ],
+  );
+}
+
 function agentDetailRow(
   label: string,
   value: string,
@@ -136,6 +161,7 @@ function renderSidebar(
     textMuted: unknown;
   },
   configInvalid: boolean,
+  compactSidebar: boolean,
 ): JSX.Element {
   const configStatusRow = buildConfigStatusRow(configInvalid, theme);
 
@@ -173,6 +199,9 @@ function renderSidebar(
       ...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);
       }),
     ],
@@ -199,15 +228,22 @@ function buildConfigStatusRow(
   );
 }
 
-export function readConfigInvalid(directory: string): boolean {
+function readConfigState(directory: string): {
+  configInvalid: boolean;
+  compactSidebar: boolean;
+} {
   let configInvalid = false;
-  loadPluginConfig(directory, {
+  const config = loadPluginConfig(directory, {
     silent: true,
     onWarning: () => {
       configInvalid = true;
     },
   });
-  return configInvalid;
+  return { configInvalid, compactSidebar: config.compactSidebar ?? false };
+}
+
+export function readConfigInvalid(directory: string): boolean {
+  return readConfigState(directory).configInvalid;
 }
 
 const plugin: TuiPluginModule & { id: string } = {
@@ -217,7 +253,7 @@ const plugin: TuiPluginModule & { id: string } = {
 
     const version = meta.version ?? (await readPackageVersion()) ?? 'dev';
     let configDirectory = getTuiDirectory(api);
-    let configInvalid = readConfigInvalid(configDirectory);
+    let { configInvalid, compactSidebar } = readConfigState(configDirectory);
     let snapshot = readTuiSnapshot();
     const renderTimer = setInterval(async () => {
       try {
@@ -225,7 +261,8 @@ const plugin: TuiPluginModule & { id: string } = {
         const currentDirectory = getTuiDirectory(api);
         if (currentDirectory !== configDirectory) {
           configDirectory = currentDirectory;
-          configInvalid = readConfigInvalid(configDirectory);
+          ({ configInvalid, compactSidebar } =
+            readConfigState(configDirectory));
         }
         api.renderer.requestRender();
       } catch {
@@ -246,6 +283,7 @@ const plugin: TuiPluginModule & { id: string } = {
             version,
             api.theme.current,
             configInvalid,
+            compactSidebar,
           );
         },
       },