From 2dad9728907dc708127876571f256df6ffa0aed5 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 30 Apr 2026 09:47:46 +0200 Subject: [PATCH] Render cockpit settings before editing exists The cockpit needs a plain terminal settings surface before interactive editing is wired, so this adds a renderer that accepts the normalized settings object and presents current plus available values in stable sections. Constraint: src/cockpit/settings.js is a sibling-lane dependency and is not present on this branch yet Constraint: User limited edits to src/cockpit/settings-render.js and test/cockpit-settings-render.test.js Rejected: Import settings metadata from src/cockpit/settings.js | dependency file is not available on this branch Confidence: high Scope-risk: narrow Tested: node --test test/cockpit-settings-render.test.js Not-tested: Integration into the live cockpit control pane --- src/cockpit/settings-render.js | 128 +++++++++++++++++++++++++++ test/cockpit-settings-render.test.js | 54 +++++++++++ 2 files changed, 182 insertions(+) create mode 100644 src/cockpit/settings-render.js create mode 100644 test/cockpit-settings-render.test.js diff --git a/src/cockpit/settings-render.js b/src/cockpit/settings-render.js new file mode 100644 index 00000000..50fa8f98 --- /dev/null +++ b/src/cockpit/settings-render.js @@ -0,0 +1,128 @@ +'use strict'; + +const DEFAULT_SETTINGS = { + theme: 'default', + sidebarWidth: 32, + refreshMs: 2000, + defaultAgent: 'codex', + defaultBase: 'main', + showLocks: true, + showWorktreePaths: true, + autopilotDefault: false, + editorCommand: '', +}; + +const SECTION_DEFINITIONS = [ + { + title: 'Appearance', + fields: [ + ['theme', 'Theme', 'default, dim, high-contrast'], + ], + }, + { + title: 'Layout', + fields: [ + ['sidebarWidth', 'Sidebar width', '20-80 columns'], + ['refreshMs', 'Refresh interval', '500-60000 ms'], + ['showWorktreePaths', 'Show worktree paths', 'true, false'], + ], + }, + { + title: 'Agents', + fields: [ + ['defaultAgent', 'Default agent', 'codex, claude, opencode, cursor, gemini'], + ['defaultBase', 'Default base', 'any branch name'], + ['autopilotDefault', 'Autopilot default', 'true, false'], + ], + }, + { + title: 'Safety', + fields: [ + ['showLocks', 'Show locks', 'true, false'], + ], + }, + { + title: 'Editor', + fields: [ + ['editorCommand', 'Editor command', 'any shell command, blank'], + ], + }, +]; + +const KEYBINDINGS = [ + '↑/↓ navigate', + 'Enter edit', + 'Esc back', + 'q quit', +]; + +function normalizeSettings(settings) { + if (!settings || typeof settings !== 'object' || Array.isArray(settings)) { + return { ...DEFAULT_SETTINGS }; + } + + return { + ...DEFAULT_SETTINGS, + ...settings, + }; +} + +function formatValue(value) { + if (value === '') { + return '(blank)'; + } + if (value === undefined || value === null) { + return '-'; + } + return String(value); +} + +function fieldLine(field, label, available, settings, selectedField) { + const marker = field === selectedField ? '>' : ' '; + return `${marker} ${label}: ${formatValue(settings[field])} (available: ${available})`; +} + +function resolveSelectedField(options) { + if (!options || typeof options !== 'object') { + return null; + } + if (typeof options.selectedField === 'string') { + return options.selectedField; + } + + return null; +} + +function renderSection(section, settings, selectedField) { + const lines = [`[${section.title}]`]; + for (const [field, label, available] of section.fields) { + lines.push(fieldLine(field, label, available, settings, selectedField)); + } + return lines.join('\n'); +} + +function renderSettingsScreen(settings, options = {}) { + const current = normalizeSettings(settings); + const selectedField = resolveSelectedField(options); + const lines = [ + 'gx cockpit settings', + 'Plain terminal settings view', + '', + ]; + + for (const section of SECTION_DEFINITIONS) { + lines.push(renderSection(section, current, selectedField)); + lines.push(''); + } + + lines.push('[Keybindings]'); + for (const keybinding of KEYBINDINGS) { + lines.push(` ${keybinding}`); + } + + return `${lines.join('\n')}\n`; +} + +module.exports = { + renderSettingsScreen, +}; diff --git a/test/cockpit-settings-render.test.js b/test/cockpit-settings-render.test.js new file mode 100644 index 00000000..1c8ba968 --- /dev/null +++ b/test/cockpit-settings-render.test.js @@ -0,0 +1,54 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const { renderSettingsScreen } = require('../src/cockpit/settings-render'); + +test('renderSettingsScreen shows settings sections and current values', () => { + const output = renderSettingsScreen({ + theme: 'dim', + sidebarWidth: 44, + refreshMs: 3000, + defaultAgent: 'claude', + defaultBase: 'dev', + showLocks: false, + showWorktreePaths: true, + autopilotDefault: true, + editorCommand: 'code --reuse-window', + }); + + assert.match(output, /gx cockpit settings/); + assert.match(output, /\[Appearance\]/); + assert.match(output, /Theme: dim \(available: default, dim, high-contrast\)/); + assert.match(output, /\[Layout\]/); + assert.match(output, /Sidebar width: 44 \(available: 20-80 columns\)/); + assert.match(output, /Refresh interval: 3000 \(available: 500-60000 ms\)/); + assert.match(output, /Show worktree paths: true \(available: true, false\)/); + assert.match(output, /\[Agents\]/); + assert.match(output, /Default agent: claude \(available: codex, claude, opencode, cursor, gemini\)/); + assert.match(output, /Default base: dev \(available: any branch name\)/); + assert.match(output, /Autopilot default: true \(available: true, false\)/); + assert.match(output, /\[Safety\]/); + assert.match(output, /Show locks: false \(available: true, false\)/); + assert.match(output, /\[Editor\]/); + assert.match(output, /Editor command: code --reuse-window \(available: any shell command, blank\)/); +}); + +test('renderSettingsScreen includes fixed keyboard hints', () => { + const output = renderSettingsScreen({}); + + assert.match(output, /\[Keybindings\]/); + assert.match(output, /↑\/↓ navigate/); + assert.match(output, /Enter edit/); + assert.match(output, /Esc back/); + assert.match(output, /q quit/); +}); + +test('renderSettingsScreen uses defaults and can mark the selected setting', () => { + const output = renderSettingsScreen(null, { selectedField: 'theme' }); + + assert.match(output, /> Theme: default \(available: default, dim, high-contrast\)/); + assert.match(output, /Editor command: \(blank\) \(available: any shell command, blank\)/); + assert.equal(output.endsWith('\n'), true); +});