From f3d64ce86ce397b313deaa534942526dcf66b1b1 Mon Sep 17 00:00:00 2001 From: Eivind Date: Wed, 6 May 2026 11:18:15 +0200 Subject: [PATCH 1/3] fix(ui): respect sort order; introduce 'status' sort mode Tabs were always reordered to push terminal-state processes (finished, stopped, failed, skipped) to the bottom, overriding sort: 'config' (and 'alphabetical'/'topological'). Reordering now only applies under the new sort: 'status' mode; explicit sort modes preserve their order strictly. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/config/validator.test.ts | 8 ++++++++ src/config/validator.ts | 2 +- src/process/manager.test.ts | 13 ++++++++++++ src/types.ts | 5 +++-- src/ui/app.ts | 2 +- src/ui/tabs.ts | 39 +++++++++++++++++------------------- 6 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/config/validator.test.ts b/src/config/validator.test.ts index 81969d8..991f6db 100644 --- a/src/config/validator.test.ts +++ b/src/config/validator.test.ts @@ -614,6 +614,14 @@ describe('validateConfig — sort', () => { expect(config.sort).toBe('topological') }) + test('preserves sort: status', () => { + const config = validateConfig({ + sort: 'status', + processes: { a: { command: 'echo a' } } + }) + expect(config.sort).toBe('status') + }) + test('throws on invalid sort value', () => { expect(() => validateConfig({ diff --git a/src/config/validator.ts b/src/config/validator.ts index 03a2143..aad9423 100644 --- a/src/config/validator.ts +++ b/src/config/validator.ts @@ -213,7 +213,7 @@ function validateErrorMatcher(name: string, value: unknown): boolean | string | return undefined } -const VALID_SORT_VALUES = new Set(['config', 'alphabetical', 'topological']) +const VALID_SORT_VALUES = new Set(['config', 'alphabetical', 'topological', 'status']) function validateSort(value: unknown): SortOrder | undefined { if (typeof value === 'string') { diff --git a/src/process/manager.test.ts b/src/process/manager.test.ts index f1bef4d..b7765c1 100644 --- a/src/process/manager.test.ts +++ b/src/process/manager.test.ts @@ -92,6 +92,19 @@ describe('ProcessManager — initialization', () => { expect(names.indexOf('db')).toBeLessThan(names.indexOf('api')) expect(names.indexOf('api')).toBeLessThan(names.indexOf('web')) }) + + test('sort: status returns config order (reorder is applied in TabBar)', () => { + const config: ResolvedNumuxConfig = { + sort: 'status', + processes: { + web: { command: 'echo web' }, + api: { command: 'echo api' }, + db: { command: 'echo db' } + } + } + const mgr = new ProcessManager(config) + expect(mgr.getProcessNames()).toEqual(['web', 'api', 'db']) + }) }) describe('ProcessManager — startAll', () => { diff --git a/src/types.ts b/src/types.ts index c4f47b1..57d1385 100644 --- a/src/types.ts +++ b/src/types.ts @@ -94,7 +94,8 @@ export interface NumuxConfig { watch?: string | string[] /** * Tab display order. `'config'` preserves definition order (package.json script order for wildcards), - * `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers. + * `'alphabetical'` sorts by process name, `'topological'` sorts by dependency tiers, + * `'status'` uses config order but moves finished/stopped/failed/skipped tabs to the bottom. * @default 'config' */ sort?: SortOrder @@ -125,7 +126,7 @@ export interface NumuxConfig { processes: Record | NumuxScriptPattern | string | true> } -export type SortOrder = 'config' | 'alphabetical' | 'topological' +export type SortOrder = 'config' | 'alphabetical' | 'topological' | 'status' /** Process config after validation — dependsOn is always normalized to an array */ export interface ResolvedProcessConfig extends Omit { diff --git a/src/ui/app.ts b/src/ui/app.ts index 917003a..55eadb2 100644 --- a/src/ui/app.ts +++ b/src/ui/app.ts @@ -70,7 +70,7 @@ export class App { // Tab bar (vertical sidebar) const processHexColors = buildProcessHexColorMap(this.names, this.config) - this.tabBar = new TabBar(this.renderer, this.names, processHexColors) + this.tabBar = new TabBar(this.renderer, this.names, processHexColors, this.config.sort === 'status') // Content row: sidebar | pane const contentRow = new BoxRenderable(this.renderer, { diff --git a/src/ui/tabs.ts b/src/ui/tabs.ts index bafd538..7f8b159 100644 --- a/src/ui/tabs.ts +++ b/src/ui/tabs.ts @@ -33,6 +33,12 @@ export const STATUS_ICON_HEX: Partial> = { /** Statuses that represent a terminal (done) state — tabs move to bottom */ export const TERMINAL_STATUSES = new Set(['finished', 'stopped', 'failed', 'skipped']) +export function getDisplayOrder(originalNames: string[], statuses: Map): string[] { + const active = originalNames.filter(n => !TERMINAL_STATUSES.has(statuses.get(n)!)) + const terminal = originalNames.filter(n => TERMINAL_STATUSES.has(statuses.get(n)!)) + return [...active, ...terminal] +} + export function formatTab(name: string, status: ProcessStatus): string { return `${STATUS_ICONS[status]} ${name}` } @@ -48,12 +54,6 @@ export function formatDescription(status: ProcessStatus, exitCode?: number | nul return desc } -export function getDisplayOrder(originalNames: string[], statuses: Map): string[] { - const active = originalNames.filter(n => !TERMINAL_STATUSES.has(statuses.get(n)!)) - const terminal = originalNames.filter(n => TERMINAL_STATUSES.has(statuses.get(n)!)) - return [...active, ...terminal] -} - export function resolveOptionColors( names: string[], statuses: Map, @@ -182,13 +182,15 @@ export class TabBar { private statuses: Map private baseDescriptions: Map private processColors: Map + private reorderByStatus: boolean private inputWaiting = new Set() private erroredProcesses = new Set() private searchMatchCounts = new Map() - constructor(renderer: CliRenderer, names: string[], colors?: Map) { + constructor(renderer: CliRenderer, names: string[], colors?: Map, reorderByStatus = false) { this.originalNames = names this.names = [...names] + this.reorderByStatus = reorderByStatus this.statuses = new Map(names.map(n => [n, 'pending' as ProcessStatus])) this.baseDescriptions = new Map(names.map(n => [n, 'pending'])) this.processColors = colors ?? new Map() @@ -270,24 +272,19 @@ export class TabBar { } private refreshOptions(): void { - // Preserve currently selected name - const currentIdx = this.renderable.getSelectedIndex() - const currentName = this.names[currentIdx] - - // Reorder: active first, terminal states at bottom - this.names = getDisplayOrder(this.originalNames, this.statuses) - + if (this.reorderByStatus) { + const currentIdx = this.renderable.getSelectedIndex() + const currentName = this.names[currentIdx] + this.names = getDisplayOrder(this.originalNames, this.statuses) + const newIdx = this.names.indexOf(currentName) + if (newIdx >= 0 && newIdx !== currentIdx) { + this.renderable.setSelectedIndex(newIdx) + } + } this.renderable.options = this.names.map(n => ({ name: formatTab(n, this.statuses.get(n)!), description: this.getDescription(n) })) - - // Restore selection by name - const newIdx = this.names.indexOf(currentName) - if (newIdx >= 0 && newIdx !== currentIdx) { - this.renderable.setSelectedIndex(newIdx) - } - this.updateOptionColors() } From cbd19fe2a139a1a1315af1844438a5dd05c50a77 Mon Sep 17 00:00:00 2001 From: Eivind Date: Wed, 6 May 2026 11:19:02 +0200 Subject: [PATCH 2/3] docs(sort): document new 'status' sort mode in README and CLI help Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 4 ++-- src/cli-flags.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 29dc7a6..0c0ffe9 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ export default defineConfig({ | Flag | Description | |------|-------------| -| `-s,` `--sort` `` | Tab display order | +| `-s,` `--sort` `` | Tab display order | | `-w,` `--workspace` `