Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
00eee3c
refactor: SettingsDialog reactive store + delete obsolete utils
psinha40898 Feb 12, 2026
456d7d0
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Feb 12, 2026
90bd195
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Feb 12, 2026
12808a0
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Feb 15, 2026
6572ac7
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Feb 16, 2026
bbbfe68
fix(cli): use checkExhaustive for default case in switch statement as…
psinha40898 Feb 16, 2026
83f511e
fix,refactor(cli): improve type safety ergonomics for non primitive s…
psinha40898 Feb 16, 2026
0895547
revert unecessary agentconfigdialog changes
psinha40898 Feb 16, 2026
7c11cf5
test(cli): the previous tests had tangled concerned with respect to u…
psinha40898 Feb 16, 2026
9f6a2bc
fix(cli): display default setting for unscoped
psinha40898 Feb 16, 2026
7de73e7
better diffs
psinha40898 Feb 17, 2026
ec9491f
test(cli): scope as the determiner of star.
psinha40898 Feb 18, 2026
1b3a5fa
test(cli): organize display tests for scope based behavior
psinha40898 Feb 18, 2026
ddac20d
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Feb 18, 2026
5e879f0
refactor(cli): Remove es lint ignores in favor of type predicate whic…
psinha40898 Feb 20, 2026
a425e43
refactor(cli): move utils for parsing settings, improve documentation…
psinha40898 Feb 21, 2026
a85ab8b
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Feb 21, 2026
cbf5ac0
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Feb 21, 2026
58e0c76
test(cli): fix a test for the non primitive settings
psinha40898 Feb 21, 2026
a0248c5
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Feb 21, 2026
ca2999a
refactor(cli): revert unecessary diffs.
psinha40898 Feb 21, 2026
8022acd
refactor(cli): rename isInScope to isInSettingsScope
psinha40898 Feb 23, 2026
d83dc8b
fix(cli): parser should not reject on falsy outputs like 0
psinha40898 Feb 23, 2026
885ad17
fix(cli): better message explaining why the prompt is showing
psinha40898 Feb 23, 2026
c68f9d3
fix(cli): import proper helper
psinha40898 Feb 23, 2026
080e5b2
fix(cli): incorrect import
psinha40898 Feb 23, 2026
f67243b
test(cli): Update tests to represent new restart prompt text
psinha40898 Feb 25, 2026
bbbdc87
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Feb 25, 2026
9cbc3c0
fix(cli): remove special case for vim mode setting in settings dialog…
psinha40898 Mar 1, 2026
aed7b76
fix(cli): VimModeContext should use new store pattern
psinha40898 Mar 1, 2026
13cab41
Merge branch 'main' into pyush/refactor/react-settings-dialog
psinha40898 Mar 1, 2026
142a568
Merge branch 'main' into pyush/refactor/react-settings-dialog
jacob314 Mar 2, 2026
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
1 change: 0 additions & 1 deletion packages/cli/src/ui/components/DialogManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,6 @@ export const DialogManager = ({
return (
<Box flexDirection="column">
<SettingsDialog
settings={settings}
onSelect={() => uiActions.closeSettingsDialog()}
onRestartRequest={async () => {
await runExitCleanup();
Expand Down
174 changes: 69 additions & 105 deletions packages/cli/src/ui/components/SettingsDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ import { render } from '../../test-utils/render.js';
import { waitFor } from '../../test-utils/async.js';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SettingsDialog } from './SettingsDialog.js';
import { LoadedSettings, SettingScope } from '../../config/settings.js';
import { SettingScope } from '../../config/settings.js';
import { createMockSettings } from '../../test-utils/settings.js';
import { VimModeProvider } from '../contexts/VimModeContext.js';
import { KeypressProvider } from '../contexts/KeypressContext.js';
import { act } from 'react';
import { saveModifiedSettings, TEST_ONLY } from '../../utils/settingsUtils.js';
import { TEST_ONLY } from '../../utils/settingsUtils.js';
import { SettingsContext } from '../contexts/SettingsContext.js';
import {
getSettingsSchema,
type SettingDefinition,
Expand Down Expand Up @@ -81,14 +82,6 @@ vi.mock('../contexts/VimModeContext.js', async () => {
};
});

vi.mock('../../utils/settingsUtils.js', async () => {
const actual = await vi.importActual('../../utils/settingsUtils.js');
return {
...actual,
saveModifiedSettings: vi.fn(),
};
});

// Shared test schemas
enum StringEnum {
FOO = 'foo',
Expand Down Expand Up @@ -185,22 +178,23 @@ const TOOLS_SHELL_FAKE_SCHEMA: SettingsSchemaType = {

// Helper function to render SettingsDialog with standard wrapper
const renderDialog = (
settings: LoadedSettings,
settings: ReturnType<typeof createMockSettings>,
onSelect: ReturnType<typeof vi.fn>,
options?: {
onRestartRequest?: ReturnType<typeof vi.fn>;
availableTerminalHeight?: number;
},
) =>
render(
<KeypressProvider>
<SettingsDialog
settings={settings}
onSelect={onSelect}
onRestartRequest={options?.onRestartRequest}
availableTerminalHeight={options?.availableTerminalHeight}
/>
</KeypressProvider>,
<SettingsContext.Provider value={settings}>
<KeypressProvider>
<SettingsDialog
onSelect={onSelect}
onRestartRequest={options?.onRestartRequest}
availableTerminalHeight={options?.availableTerminalHeight}
/>
</KeypressProvider>
</SettingsContext.Provider>,
);

describe('SettingsDialog', () => {
Expand Down Expand Up @@ -361,9 +355,8 @@ describe('SettingsDialog', () => {

describe('Settings Toggling', () => {
it('should toggle setting with Enter key', async () => {
vi.mocked(saveModifiedSettings).mockClear();

const settings = createMockSettings();
const setValueSpy = vi.spyOn(settings, 'setValue');
const onSelect = vi.fn();

const { stdin, unmount, lastFrame } = renderDialog(settings, onSelect);
Expand All @@ -377,27 +370,16 @@ describe('SettingsDialog', () => {
act(() => {
stdin.write(TerminalKeys.ENTER as string);
});
// Wait for the setting change to be processed
await waitFor(() => {
expect(
vi.mocked(saveModifiedSettings).mock.calls.length,
).toBeGreaterThan(0);
});

// Wait for the mock to be called
// Wait for setValue to be called
await waitFor(() => {
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
expect(setValueSpy).toHaveBeenCalled();
});

expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
new Set<string>(['general.vimMode']),
expect.objectContaining({
general: expect.objectContaining({
vimMode: true,
}),
}),
expect.any(LoadedSettings),
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'general.vimMode',
true,
);

unmount();
Expand All @@ -416,13 +398,13 @@ describe('SettingsDialog', () => {
expectedValue: StringEnum.FOO,
},
])('$name', async ({ initialValue, expectedValue }) => {
vi.mocked(saveModifiedSettings).mockClear();
vi.mocked(getSettingsSchema).mockReturnValue(ENUM_FAKE_SCHEMA);

const settings = createMockSettings();
if (initialValue !== undefined) {
settings.setValue(SettingScope.User, 'ui.theme', initialValue);
}
const setValueSpy = vi.spyOn(settings, 'setValue');

const onSelect = vi.fn();

Expand All @@ -434,20 +416,13 @@ describe('SettingsDialog', () => {
});

await waitFor(() => {
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalled();
expect(setValueSpy).toHaveBeenCalledWith(
SettingScope.User,
'ui.theme',
expectedValue,
);
});

expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
new Set<string>(['ui.theme']),
expect.objectContaining({
ui: expect.objectContaining({
theme: expectedValue,
}),
}),
expect.any(LoadedSettings),
SettingScope.User,
);

unmount();
});
});
Expand Down Expand Up @@ -669,11 +644,13 @@ describe('SettingsDialog', () => {
const onSelect = vi.fn();

const { stdin, unmount } = render(
<VimModeProvider settings={settings}>
<KeypressProvider>
<SettingsDialog settings={settings} onSelect={onSelect} />
</KeypressProvider>
</VimModeProvider>,
<SettingsContext.Provider value={settings}>
<VimModeProvider settings={settings}>
<KeypressProvider>
<SettingsDialog onSelect={onSelect} />
</KeypressProvider>
</VimModeProvider>
</SettingsContext.Provider>,
);

// Navigate to and toggle vim mode setting
Expand Down Expand Up @@ -826,58 +803,41 @@ describe('SettingsDialog', () => {
pager: 'less',
},
},
])(
'should $name',
async ({ toggleCount, shellSettings, expectedSiblings }) => {
vi.mocked(saveModifiedSettings).mockClear();

vi.mocked(getSettingsSchema).mockReturnValue(TOOLS_SHELL_FAKE_SCHEMA);

const settings = createMockSettings({
tools: {
shell: shellSettings,
},
});
])('should $name', async ({ toggleCount, shellSettings }) => {
vi.mocked(getSettingsSchema).mockReturnValue(TOOLS_SHELL_FAKE_SCHEMA);

const onSelect = vi.fn();
const settings = createMockSettings({
tools: {
shell: shellSettings,
},
});
const setValueSpy = vi.spyOn(settings, 'setValue');

const { stdin, unmount } = renderDialog(settings, onSelect);
const onSelect = vi.fn();

for (let i = 0; i < toggleCount; i++) {
act(() => {
stdin.write(TerminalKeys.ENTER as string);
});
}
const { stdin, unmount } = renderDialog(settings, onSelect);

await waitFor(() => {
expect(
vi.mocked(saveModifiedSettings).mock.calls.length,
).toBeGreaterThan(0);
for (let i = 0; i < toggleCount; i++) {
act(() => {
stdin.write(TerminalKeys.ENTER as string);
});
}

const calls = vi.mocked(saveModifiedSettings).mock.calls;
calls.forEach((call) => {
const [modifiedKeys, pendingSettings] = call;

if (modifiedKeys.has('tools.shell.showColor')) {
const shellSettings = pendingSettings.tools?.shell as
| Record<string, unknown>
| undefined;

Object.entries(expectedSiblings).forEach(([key, value]) => {
expect(shellSettings?.[key]).toBe(value);
expect(modifiedKeys.has(`tools.shell.${key}`)).toBe(false);
});

expect(modifiedKeys.size).toBe(1);
}
});
await waitFor(() => {
expect(setValueSpy).toHaveBeenCalled();
});

expect(calls.length).toBeGreaterThan(0);
// With the store pattern, setValue is called atomically per key.
// Sibling preservation is handled by LoadedSettings internally.
const calls = setValueSpy.mock.calls;
expect(calls.length).toBeGreaterThan(0);
calls.forEach((call) => {
// Each call should target only 'tools.shell.showColor'
expect(call[1]).toBe('tools.shell.showColor');
});

unmount();
},
);
unmount();
});
});

describe('Keyboard Shortcuts Edge Cases', () => {
Expand Down Expand Up @@ -1157,9 +1117,11 @@ describe('SettingsDialog', () => {
const onSelect = vi.fn();

const { stdin, unmount, rerender } = render(
<KeypressProvider>
<SettingsDialog settings={settings} onSelect={onSelect} />
</KeypressProvider>,
<SettingsContext.Provider value={settings}>
<KeypressProvider>
<SettingsDialog onSelect={onSelect} />
</KeypressProvider>
</SettingsContext.Provider>,
);

// Navigate to the last setting
Expand All @@ -1184,9 +1146,11 @@ describe('SettingsDialog', () => {
},
});
rerender(
<KeypressProvider>
<SettingsDialog settings={settings} onSelect={onSelect} />
</KeypressProvider>,
<SettingsContext.Provider value={settings}>
<KeypressProvider>
<SettingsDialog onSelect={onSelect} />
</KeypressProvider>
</SettingsContext.Provider>,
);

// Press Escape to exit
Expand Down
Loading