Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).

## Unreleased

### New Features

- **Light TUI palette** — a high-contrast palette for terminals with light backgrounds. Select it through `/config palette`, or persist it before launching the dashboard with `orch config global set palette light`.

### Bug Fixes

- **Readable light-palette tabs** — active and flashing header tabs use a semantic solid-fill foreground, keeping text above the WCAG AA 4.5:1 contrast threshold on every saturated light-palette status color.
- **Stable palette transitions** — light-palette gray and ghost tokens remain distinct, so live color remapping preserves their intended semantic colors when switching palettes.

### Security

- Updated `js-yaml`, `liquidjs`, Vitest, Vite, esbuild, and tsx to patched releases. The complete production and development dependency graph now passes `npm audit` with verified registry signatures.

### Tests

- Added contrast and palette-remapping regression coverage.
- Verified the production CLI in a real pseudo-terminal, including persisted light-palette loading, emitted ANSI colors, activity filtering, and clean shutdown.
- Full suite: 2078 passed, 2 skipped. Coverage: 69.72% statements, 63.37% branches, 69.64% functions, and 71.94% lines.

## 1.0.31 (2026-07-27)

### New Features
Expand Down
1,658 changes: 654 additions & 1,004 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,24 @@
"chalk": "^5.4.1",
"commander": "^13.1.0",
"ink": "^6.8.0",
"js-yaml": "^4.1.0",
"liquidjs": "^10.21.0",
"js-yaml": "^4.3.0",
"liquidjs": "^10.27.2",
"nanoid": "^5.1.5",
"react": "^19.2.4"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.17.0",
"@types/react": "^19.2.14",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/coverage-v8": "^4.1.10",
"ink-testing-library": "^4.0.0",
"tsup": "^8.4.0",
"tsx": "^4.19.0",
"tsx": "^4.23.1",
"typescript": "^5.7.0",
"vitest": "^3.0.0"
"vitest": "^4.1.10"
},
"overrides": {
"esbuild": "^0.28.1",
"vite": "6.4.3"
}
}
21 changes: 19 additions & 2 deletions src/cli/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

import type { Command } from 'commander';
import type { LightContainer } from '../../container.js';
import type { ActivityFilterPreset } from '../../domain/global-config.js';
import {
isTuiPaletteName,
TUI_PALETTE_NAMES,
type ActivityFilterPreset,
} from '../../domain/global-config.js';
import { printSuccess, printError, dim } from '../output.js';
import { spawn } from 'node:child_process';

Expand Down Expand Up @@ -84,7 +88,11 @@ export function registerConfigCommand(program: Command, container: LightContaine
.description('Get a global config value')
.action(async (key: string) => {
const gc = await container.globalConfigStore.read();
const value = key === 'activity_filter' ? gc.tui.activity_filter : undefined;
const value = key === 'activity_filter'
? gc.tui.activity_filter
: key === 'palette'
? gc.tui.palette
: undefined;
if (container.context.json) {
console.log(JSON.stringify({ key, value }));
} else {
Expand All @@ -103,6 +111,14 @@ export function registerConfigCommand(program: Command, container: LightContaine
}
await container.globalConfigStore.set('activity_filter', value as ActivityFilterPreset);
printSuccess(`${key} = ${value}`);
} else if (key === 'palette') {
const palette = value.toLowerCase();
if (!isTuiPaletteName(palette)) {
printError(`Invalid value "${value}". Valid: ${TUI_PALETTE_NAMES.join(', ')}`);
return;
}
await container.globalConfigStore.set('palette', palette);
printSuccess(`${key} = ${palette}`);
} else {
printError(`Unknown global config key: ${key}`);
}
Expand All @@ -117,6 +133,7 @@ export function registerConfigCommand(program: Command, container: LightContaine
console.log(JSON.stringify(gc));
} else {
console.log(` ${dim('tui.activity_filter')} = ${gc.tui.activity_filter}`);
console.log(` ${dim('tui.palette')} = ${gc.tui.palette}`);
}
});
}
3 changes: 2 additions & 1 deletion src/domain/global-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@
export type ActivityFilterPreset = 'all' | 'text' | 'tools' | 'errors' | 'events';

/** Built-in TUI color palette name. */
export type TuiPaletteName = 'amber' | 'ocean' | 'forest' | 'violet';
export type TuiPaletteName = 'amber' | 'ocean' | 'forest' | 'violet' | 'light';

export const TUI_PALETTE_NAMES: readonly TuiPaletteName[] = [
'amber',
'ocean',
'forest',
'violet',
'light',
];

export function isTuiPaletteName(value: unknown): value is TuiPaletteName {
Expand Down
2 changes: 1 addition & 1 deletion src/tui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3628,7 +3628,7 @@ function ActivityFeed({ messages, height, width, agents, agentNameMap, agentColo

// Subtle zebra: odd groups get barely-visible tint, errors override
const isOddGroup = (groupIndices[i]! & 1) === 1;
const rowBg = getMsgBg(msgType) ?? (isOddGroup ? '#1a1a1a' : undefined);
const rowBg = getMsgBg(msgType) ?? (isOddGroup ? tuiColors.alternatingRowBg : undefined);

const relTs = relativeTime(msg.ts, now);
const displayText = capLine(msg.text, textW);
Expand Down
46 changes: 41 additions & 5 deletions src/tui/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* TUI color palette — "Command & Control" theme.
*
* Dark-first design with amber brand accent and strategic color pops.
* The explicit light palette uses dark foregrounds and pale chip backgrounds
* for terminals whose default background is light.
* Hex equivalents of ANSI 256 palette for Ink compatibility.
*/

Expand Down Expand Up @@ -31,7 +33,9 @@ export interface TuiColorPalette {
toolBg: string;
accentBg: string;
neutralBg: string;
darkText: string;
alternatingRowBg: string;
/** Foreground rendered on saturated solid fills. */
solidText: string;
pink: string;
olive: string;
orange: string;
Expand Down Expand Up @@ -60,7 +64,8 @@ export const TUI_PALETTES: Readonly<Record<TuiPaletteName, Readonly<TuiColorPale
toolBg: '#0f1f2d',
accentBg: '#2d1f0a',
neutralBg: '#1a1a22',
darkText: '#0a0a0c',
alternatingRowBg: '#1a1a1a',
solidText: '#0a0a0c',
pink: '#d787af',
olive: '#afaf5f',
orange: '#d7875f',
Expand All @@ -87,7 +92,8 @@ export const TUI_PALETTES: Readonly<Record<TuiPaletteName, Readonly<TuiColorPale
toolBg: '#102f46',
accentBg: '#12304a',
neutralBg: '#182631',
darkText: '#07131d',
alternatingRowBg: '#141d2b',
solidText: '#07131d',
pink: '#ff87d7',
olive: '#afd787',
orange: '#ff9f5f',
Expand All @@ -114,7 +120,8 @@ export const TUI_PALETTES: Readonly<Record<TuiPaletteName, Readonly<TuiColorPale
toolBg: '#15302a',
accentBg: '#14351f',
neutralBg: '#1c2920',
darkText: '#09150c',
alternatingRowBg: '#172219',
solidText: '#09150c',
pink: '#d787af',
olive: '#afd75f',
orange: '#d79f5f',
Expand All @@ -141,11 +148,40 @@ export const TUI_PALETTES: Readonly<Record<TuiPaletteName, Readonly<TuiColorPale
toolBg: '#1c2342',
accentBg: '#30204a',
neutralBg: '#261e30',
darkText: '#130a1d',
alternatingRowBg: '#1d1826',
solidText: '#130a1d',
pink: '#ff87d7',
olive: '#afd787',
orange: '#ff9f7f',
},
light: {
amber: '#9a6700',
amberDim: '#7a5200',
green: '#1a7f37',
red: '#b42318',
blue: '#175cd3',
yellow: '#946200',
cyan: '#007a85',
purple: '#6941c6',
white: '#1d2939',
silver: '#344054',
gray: '#5d6678',
dim: '#475467',
ghost: '#667085',
void: '#f2f4f7',
errorBg: '#fef3f2',
warnBg: '#fffaeb',
successBg: '#ecfdf3',
infoBg: '#eff8ff',
toolBg: '#eef4ff',
accentBg: '#fff7e0',
neutralBg: '#f2f4f7',
alternatingRowBg: '#f8fafc',
solidText: '#ffffff',
pink: '#c11574',
olive: '#5f6f13',
orange: '#b54708',
},
};

/** Mutable live palette. Imports keep the same object reference across TUI rerenders. */
Expand Down
4 changes: 2 additions & 2 deletions src/tui/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function FlashingTabLabel({

if (isBright) {
return (
<Text backgroundColor={flashColor} color={tuiColors.darkText} bold>
<Text backgroundColor={flashColor} color={tuiColors.solidText} bold>
{' '}{tab.key} {tab.label}{badge}{' '}
</Text>
);
Expand Down Expand Up @@ -226,7 +226,7 @@ function BrandBar({
<React.Fragment key={tab.id}>
{i > 0 && <Text>{' '}</Text>}
{isActive ? (
<Text backgroundColor={tuiColors.amber} color={tuiColors.darkText} bold>
<Text backgroundColor={tuiColors.amber} color={tuiColors.solidText} bold>
{' '}{tab.key} {tab.label}{badge}{' '}
</Text>
) : isFlashing ? (
Expand Down
1 change: 1 addition & 0 deletions src/tui/wizardConfigs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,7 @@ const PALETTE_OPTIONS = [
{ value: 'ocean', label: 'Ocean', hint: 'cool blue control room' },
{ value: 'forest', label: 'Forest', hint: 'calm green operations' },
{ value: 'violet', label: 'Violet', hint: 'high-contrast purple' },
{ value: 'light', label: 'Light', hint: 'high-contrast palette for light terminals' },
];

// ── Notification toggle options ──
Expand Down
27 changes: 26 additions & 1 deletion test/unit/cli/commands-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Command } from 'commander';
import { registerConfigCommand } from '../../../src/cli/commands/config.js';
import { makeContainer } from './helpers.js';
Expand All @@ -16,6 +16,10 @@ describe('config command', () => {
registerConfigCommand(program, container);
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('config get', () => {
it('calls configStore.get with key', async () => {
await program.parseAsync(['config', 'get', 'scheduling.poll_interval_ms'], { from: 'user' });
Expand Down Expand Up @@ -56,4 +60,25 @@ describe('config command', () => {
expect(container.configStore.set).toHaveBeenCalledWith('flag', true);
});
});

describe('config global', () => {
it('persists a valid TUI palette', async () => {
await program.parseAsync(['config', 'global', 'set', 'palette', 'light'], { from: 'user' });

expect(container.globalConfigStore.set).toHaveBeenCalledWith('palette', 'light');
});

it('rejects an unknown TUI palette', async () => {
await program.parseAsync(['config', 'global', 'set', 'palette', 'solarized'], { from: 'user' });

expect(container.globalConfigStore.set).not.toHaveBeenCalled();
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Valid: amber, ocean, forest, violet, light'));
});

it('reads the current TUI palette', async () => {
await program.parseAsync(['config', 'global', 'get', 'palette'], { from: 'user' });

expect(console.log).toHaveBeenCalledWith(expect.stringContaining('"amber"'));
});
});
});
6 changes: 5 additions & 1 deletion test/unit/cli/commands-context.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Command } from 'commander';
import { registerContextCommand } from '../../../src/cli/commands/context.js';
import type { ContextEntry } from '../../../src/infrastructure/storage/interfaces.js';
Expand Down Expand Up @@ -29,6 +29,10 @@ describe('context command', () => {
registerContextCommand(program, container);
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('context set', () => {
it('calls contextStore.set with key and value', async () => {
await program.parseAsync(['context', 'set', 'mykey', 'myval'], { from: 'user' });
Expand Down
26 changes: 13 additions & 13 deletions test/unit/cli/commands-init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ const mocks = vi.hoisted(() => {
const atomicWrite = vi.fn(async () => {});
const agentPathFn = vi.fn((id: string) => `/mock/.orchestry/agents/${id}.yml`);

const MockPaths = vi.fn(() => ({
root: '/mock/.orchestry',
tasksDir: '/mock/.orchestry/tasks',
agentsDir: '/mock/.orchestry/agents',
runsDir: '/mock/.orchestry/runs',
templatesDir: '/mock/.orchestry/templates',
logsDir: '/mock/.orchestry/logs',
configPath: '/mock/.orchestry/config.yml',
gitignorePath: '/mock/.orchestry/.gitignore',
workspaceExcludePath: '/mock/.orchestry/.workspace-exclude',
defaultTemplatePath: vi.fn(() => '/mock/.orchestry/templates/default.md'),
agentPath: agentPathFn,
}));
class MockPaths {
readonly root = '/mock/.orchestry';
readonly tasksDir = '/mock/.orchestry/tasks';
readonly agentsDir = '/mock/.orchestry/agents';
readonly runsDir = '/mock/.orchestry/runs';
readonly templatesDir = '/mock/.orchestry/templates';
readonly logsDir = '/mock/.orchestry/logs';
readonly configPath = '/mock/.orchestry/config.yml';
readonly gitignorePath = '/mock/.orchestry/.gitignore';
readonly workspaceExcludePath = '/mock/.orchestry/.workspace-exclude';
readonly defaultTemplatePath = vi.fn(() => '/mock/.orchestry/templates/default.md');
readonly agentPath = agentPathFn;
}

const execFile = vi.fn((_cmd: string, _args: string[], _opts: unknown, cb: (err: Error | null) => void) => {
cb(null);
Expand Down
6 changes: 5 additions & 1 deletion test/unit/cli/commands-logs.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Command } from 'commander';
import { registerLogsCommand } from '../../../src/cli/commands/logs.js';
import type { RunEvent } from '../../../src/domain/run.js';
Expand Down Expand Up @@ -36,6 +36,10 @@ describe('logs command', () => {
registerLogsCommand(program, container);
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('logs <run-id>', () => {
it('shows events for a specific run (uses readEventsTail without --since)', async () => {
(container.runService.readEventsTail as ReturnType<typeof vi.fn>).mockResolvedValue([
Expand Down
10 changes: 10 additions & 0 deletions test/unit/cli/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ export function makeContainer(overrides: Partial<Container> = {}): Container {
read: vi.fn(async () => ({})),
write: vi.fn(async () => {}),
},
globalConfigStore: {
read: vi.fn(async () => ({
tui: {
palette: 'amber',
activity_filter: 'all',
notifications: { toast: true, bell: false },
},
})),
set: vi.fn(async () => {}),
},
contextStore: {
get: vi.fn(async () => null),
set: vi.fn(async () => {}),
Expand Down
2 changes: 1 addition & 1 deletion test/unit/domain/global-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('DEFAULT_GLOBAL_CONFIG', () => {
});

it('validates every built-in palette name', () => {
expect(TUI_PALETTE_NAMES).toEqual(['amber', 'ocean', 'forest', 'violet']);
expect(TUI_PALETTE_NAMES).toEqual(['amber', 'ocean', 'forest', 'violet', 'light']);
for (const palette of TUI_PALETTE_NAMES) {
expect(isTuiPaletteName(palette)).toBe(true);
}
Expand Down
Loading
Loading