From d60a164025621bf951b43c0f7f0dae2b4ba8ea67 Mon Sep 17 00:00:00 2001 From: timkjr Date: Tue, 15 Sep 2026 21:55:32 -0500 Subject: [PATCH] feat(split-pane): add pure divider-clamp and picker-list helpers --- src/web/public/constants.js | 25 ++++++++++++ test/split-pane-helpers.test.ts | 67 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 test/split-pane-helpers.test.ts diff --git a/src/web/public/constants.js b/src/web/public/constants.js index 6521ccc1..1fac74a3 100644 --- a/src/web/public/constants.js +++ b/src/web/public/constants.js @@ -1516,8 +1516,33 @@ function terminalLogicalLine(buffer, row, cols, maxRows) { return { startRow, endRow, text, offsetToCell, cellToOffset }; } +// ═══════════════════════════════════════════════════════════════ +// Split-Pane Sessions — pure helpers (divider math, picker list) +// ═══════════════════════════════════════════════════════════════ + +function clampDividerPercent(rawPercent, min = 20, max = 80) { + if (rawPercent < min) return min; + if (rawPercent > max) return max; + return rawPercent; +} + +function buildSplitPickerSessions(sessions, sessionOrder, excludeId) { + const result = []; + for (const id of sessionOrder) { + if (id === excludeId) continue; + const session = sessions.get(id); + if (!session) continue; + result.push({ id, label: session.name || 'Session' }); + } + return result; +} + if (typeof window !== 'undefined') { window.CodemanHistoryFormat = { formatHistoryBytes, computeHistoryTruncationNotice, computeRewriteScrollLine }; window.CodemanFilePaths = { absoluteFilePathPattern, previewsInFileViewer, FILE_PREVIEW_EXTENSIONS }; window.CodemanTerminalLines = { terminalLogicalLine }; + window.CodemanSplitPane = { + clampDividerPercent, + buildSplitPickerSessions, + }; } diff --git a/test/split-pane-helpers.test.ts b/test/split-pane-helpers.test.ts new file mode 100644 index 00000000..22c27150 --- /dev/null +++ b/test/split-pane-helpers.test.ts @@ -0,0 +1,67 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import vm from 'node:vm'; +import { describe, expect, it } from 'vitest'; + +function loadSplitPaneHelper() { + const context = vm.createContext({ window: {}, globalThis: {} }); + const source = readFileSync(resolve(import.meta.dirname, '../src/web/public/constants.js'), 'utf8'); + vm.runInContext(source, context, { filename: 'constants.js' }); + return (context.window as { CodemanSplitPane: any }).CodemanSplitPane; +} + +describe('CodemanSplitPane.clampDividerPercent', () => { + it('passes through a value inside the clamp range', () => { + const { clampDividerPercent } = loadSplitPaneHelper(); + expect(clampDividerPercent(50)).toBe(50); + expect(clampDividerPercent(35.5)).toBe(35.5); + }); + + it('clamps below the floor to the floor', () => { + const { clampDividerPercent } = loadSplitPaneHelper(); + expect(clampDividerPercent(5)).toBe(20); + }); + + it('clamps above the ceiling to the ceiling', () => { + const { clampDividerPercent } = loadSplitPaneHelper(); + expect(clampDividerPercent(95)).toBe(80); + }); + + it('honors custom min/max', () => { + const { clampDividerPercent } = loadSplitPaneHelper(); + expect(clampDividerPercent(10, 15, 85)).toBe(15); + expect(clampDividerPercent(90, 15, 85)).toBe(85); + }); +}); + +describe('CodemanSplitPane.buildSplitPickerSessions', () => { + it('excludes the active session and preserves tab order', () => { + const { buildSplitPickerSessions } = loadSplitPaneHelper(); + const sessions = new Map([ + ['a', { name: 'w1-codeman' }], + ['b', { name: 'w1-mcp-memory' }], + ['c', { name: null }], + ]); + const sessionOrder = ['a', 'b', 'c']; + const result = buildSplitPickerSessions(sessions, sessionOrder, 'a'); + expect(result).toEqual([ + { id: 'b', label: 'w1-mcp-memory' }, + { id: 'c', label: 'Session' }, + ]); + }); + + it('drops order entries with no matching session (stale ids)', () => { + const { buildSplitPickerSessions } = loadSplitPaneHelper(); + const sessions = new Map([['a', { name: 'w1-codeman' }]]); + const sessionOrder = ['a', 'ghost']; + const result = buildSplitPickerSessions(sessions, sessionOrder, null); + expect(result).toEqual([{ id: 'a', label: 'w1-codeman' }]); + }); + + it('returns an empty list when only the excluded session exists', () => { + const { buildSplitPickerSessions } = loadSplitPaneHelper(); + const sessions = new Map([['a', { name: 'w1-codeman' }]]); + const result = buildSplitPickerSessions(sessions, ['a'], 'a'); + expect(result).toEqual([]); + }); +});