feat(sidebar): add single-project display mode
This commit is contained in:
@@ -207,7 +207,8 @@ Managed health failures are classified as `timeout`, `connection_refused`, `conn
|
||||
- `readSettingsFromDiskMigrated()`
|
||||
- `writeSettingsToDisk(settings)`
|
||||
- `persistSettings(changes)`
|
||||
- Persistent permission auto-accept policy is stored under `permissionAutoAccept`; execution ownership lives in `lib/permission-auto-accept/`.
|
||||
- Persistent permission auto-accept policy is stored under `permissionAutoAccept`; execution ownership lives in `lib/permission-auto-accept/`.
|
||||
- Shared sidebar preferences are stored as validated top-level fields: `sidebarProjectDisplayMode`, `sidebarSessionGroupingMode`, `sidebarProjectSortOrder`, and `sidebarShowRecentSection`. Device-local picker selection and sticky-header state do not enter `settings.json`.
|
||||
|
||||
## Public exports (settings-helpers.js)
|
||||
- `createSettingsHelpers(dependencies)`: creates settings helper runtime for settings request/response shaping.
|
||||
|
||||
@@ -29,6 +29,9 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
const PWA_ORIENTATION_VALUES = new Set(['system', 'portrait', 'landscape']);
|
||||
const MOBILE_KEYBOARD_MODE_VALUES = new Set(['native', 'resize-content']);
|
||||
const TERMINAL_SHELL_VALUES = new Set(['auto', 'bash', 'zsh', 'sh', 'fish', 'pwsh', 'powershell', 'cmd', 'dash', 'ksh', 'nu']);
|
||||
const SIDEBAR_PROJECT_DISPLAY_MODE_VALUES = new Set(['all', 'single']);
|
||||
const SIDEBAR_SESSION_GROUPING_MODE_VALUES = new Set(['by-worktree', 'flat']);
|
||||
const SIDEBAR_PROJECT_SORT_ORDER_VALUES = new Set(['manual', 'a-z', 'z-a', 'date-added', 'recent']);
|
||||
const HIDDEN_MODELS_MAX = 1024;
|
||||
const RECENT_EFFORTS_MAX_KEYS = 128;
|
||||
const RECENT_EFFORTS_MAX_VARIANTS_PER_KEY = 5;
|
||||
@@ -243,6 +246,18 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
if (typeof candidate.activeProjectId === 'string' && candidate.activeProjectId.length > 0) {
|
||||
result.activeProjectId = candidate.activeProjectId;
|
||||
}
|
||||
if (SIDEBAR_PROJECT_DISPLAY_MODE_VALUES.has(candidate.sidebarProjectDisplayMode)) {
|
||||
result.sidebarProjectDisplayMode = candidate.sidebarProjectDisplayMode;
|
||||
}
|
||||
if (SIDEBAR_SESSION_GROUPING_MODE_VALUES.has(candidate.sidebarSessionGroupingMode)) {
|
||||
result.sidebarSessionGroupingMode = candidate.sidebarSessionGroupingMode;
|
||||
}
|
||||
if (SIDEBAR_PROJECT_SORT_ORDER_VALUES.has(candidate.sidebarProjectSortOrder)) {
|
||||
result.sidebarProjectSortOrder = candidate.sidebarProjectSortOrder;
|
||||
}
|
||||
if (typeof candidate.sidebarShowRecentSection === 'boolean') {
|
||||
result.sidebarShowRecentSection = candidate.sidebarShowRecentSection;
|
||||
}
|
||||
|
||||
if (Array.isArray(candidate.securityScopedBookmarks)) {
|
||||
result.securityScopedBookmarks = normalizeStringArray(candidate.securityScopedBookmarks);
|
||||
|
||||
@@ -66,6 +66,28 @@ describe('settings helpers', () => {
|
||||
expect(helpers.sanitizeSettingsUpdate({ draftStartersVisible: 'false' })).toEqual({});
|
||||
});
|
||||
|
||||
it('sanitizes shared sidebar display preferences', () => {
|
||||
const helpers = createTestHelpers();
|
||||
|
||||
expect(helpers.sanitizeSettingsUpdate({
|
||||
sidebarProjectDisplayMode: 'single',
|
||||
sidebarSessionGroupingMode: 'flat',
|
||||
sidebarProjectSortOrder: 'z-a',
|
||||
sidebarShowRecentSection: false,
|
||||
})).toEqual({
|
||||
sidebarProjectDisplayMode: 'single',
|
||||
sidebarSessionGroupingMode: 'flat',
|
||||
sidebarProjectSortOrder: 'z-a',
|
||||
sidebarShowRecentSection: false,
|
||||
});
|
||||
expect(helpers.sanitizeSettingsUpdate({
|
||||
sidebarProjectDisplayMode: 'grid',
|
||||
sidebarSessionGroupingMode: 'project',
|
||||
sidebarProjectSortOrder: 'random',
|
||||
sidebarShowRecentSection: 'false',
|
||||
})).toEqual({});
|
||||
});
|
||||
|
||||
it('accepts only booleans for wide chat layout', () => {
|
||||
const helpers = createTestHelpers();
|
||||
|
||||
|
||||
@@ -39,6 +39,24 @@ const createRuntime = async () => {
|
||||
};
|
||||
|
||||
describe('settings runtime', () => {
|
||||
it('round-trips shared sidebar preferences through settings.json', async () => {
|
||||
const { runtime, settingsFilePath, cleanup } = await createRuntime();
|
||||
const preferences = {
|
||||
sidebarProjectDisplayMode: 'single',
|
||||
sidebarSessionGroupingMode: 'flat',
|
||||
sidebarProjectSortOrder: 'date-added',
|
||||
sidebarShowRecentSection: false,
|
||||
};
|
||||
try {
|
||||
await runtime.persistSettings(preferences);
|
||||
|
||||
await expect(runtime.readSettingsFromDisk()).resolves.toEqual(preferences);
|
||||
await expect(fsPromises.readFile(settingsFilePath, 'utf8')).resolves.toBe(JSON.stringify(preferences, null, 2));
|
||||
} finally {
|
||||
await cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it.skipIf(process.platform === 'win32')('writes settings with restrictive directory and file permissions', async () => {
|
||||
const { runtime, settingsFilePath, tempRoot, cleanup } = await createRuntime();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user