Skip to content

Commit dc38c79

Browse files
authored
Merge pull request #59 from proxikal/feat/auto-expand-ai-groups-setting
feat: add auto-expand AI response groups setting
2 parents 9c04e90 + 93b515a commit dc38c79

9 files changed

Lines changed: 91 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
77
## [Unreleased]
88

99
### Added
10+
- `general.autoExpandAIGroups` setting: automatically expands all AI response groups when opening a transcript or when new AI responses arrive in a live session. Defaults to off. Stored in the on-disk config so it persists across restarts.
11+
12+
1013
- Strict IPC input validation guards for project/session/subagent/search limits.
1114
- `get-waterfall-data` IPC endpoint implementation.
1215
- Cross-platform path normalization in renderer path resolvers.

src/main/ipc/configValidation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ function validateGeneralSection(data: unknown): ValidationSuccess<'general'> | V
203203
'theme',
204204
'defaultTab',
205205
'claudeRootPath',
206+
'autoExpandAIGroups',
206207
];
207208

208209
const result: Partial<GeneralConfig> = {};
@@ -267,6 +268,12 @@ function validateGeneralSection(data: unknown): ValidationSuccess<'general'> | V
267268
result.claudeRootPath = path.resolve(normalized);
268269
}
269270
break;
271+
case 'autoExpandAIGroups':
272+
if (typeof value !== 'boolean') {
273+
return { valid: false, error: `general.${key} must be a boolean` };
274+
}
275+
result.autoExpandAIGroups = value;
276+
break;
270277
default:
271278
return { valid: false, error: `Unsupported general key: ${key}` };
272279
}

src/main/services/infrastructure/ConfigManager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export interface GeneralConfig {
181181
theme: 'dark' | 'light' | 'system';
182182
defaultTab: 'dashboard' | 'last-session';
183183
claudeRootPath: string | null;
184+
autoExpandAIGroups: boolean;
184185
}
185186

186187
export interface DisplayConfig {
@@ -248,6 +249,7 @@ const DEFAULT_CONFIG: AppConfig = {
248249
theme: 'dark',
249250
defaultTab: 'dashboard',
250251
claudeRootPath: null,
252+
autoExpandAIGroups: false,
251253
},
252254
display: {
253255
showTimestamps: true,

src/renderer/components/settings/hooks/useSettingsConfig.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface SafeConfig {
3030
theme: 'dark' | 'light' | 'system';
3131
defaultTab: 'dashboard' | 'last-session';
3232
claudeRootPath: string | null;
33+
autoExpandAIGroups: boolean;
3334
};
3435
notifications: {
3536
enabled: boolean;
@@ -154,6 +155,7 @@ export function useSettingsConfig(): UseSettingsConfigReturn {
154155
theme: displayConfig?.general?.theme ?? 'dark',
155156
defaultTab: displayConfig?.general?.defaultTab ?? 'dashboard',
156157
claudeRootPath: displayConfig?.general?.claudeRootPath ?? null,
158+
autoExpandAIGroups: displayConfig?.general?.autoExpandAIGroups ?? false,
157159
},
158160
notifications: {
159161
enabled: displayConfig?.notifications?.enabled ?? true,

src/renderer/components/settings/hooks/useSettingsHandlers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ export function useSettingsHandlers({
287287
theme: 'dark',
288288
defaultTab: 'dashboard',
289289
claudeRootPath: null,
290+
autoExpandAIGroups: false,
290291
},
291292
display: {
292293
showTimestamps: true,

src/renderer/components/settings/sections/GeneralSection.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { SettingRow, SettingsSectionHeader, SettingsSelect, SettingsToggle } fro
1515
import type { SafeConfig } from '../hooks/useSettingsConfig';
1616
import type { ClaudeRootInfo, WslClaudeRootCandidate } from '@shared/types';
1717
import type { HttpServerStatus } from '@shared/types/api';
18+
import type { AppConfig } from '@shared/types/notifications';
1819

1920
// Theme options
2021
const THEME_OPTIONS = [
@@ -26,7 +27,7 @@ const THEME_OPTIONS = [
2627
interface GeneralSectionProps {
2728
readonly safeConfig: SafeConfig;
2829
readonly saving: boolean;
29-
readonly onGeneralToggle: (key: 'launchAtLogin' | 'showDockIcon', value: boolean) => void;
30+
readonly onGeneralToggle: (key: keyof AppConfig['general'], value: boolean) => void;
3031
readonly onThemeChange: (value: 'dark' | 'light' | 'system') => void;
3132
}
3233

@@ -286,6 +287,16 @@ export const GeneralSection = ({
286287
disabled={saving}
287288
/>
288289
</SettingRow>
290+
<SettingRow
291+
label="Expand AI responses by default"
292+
description="Automatically expand each response turn when opening a transcript or receiving a new message"
293+
>
294+
<SettingsToggle
295+
enabled={safeConfig.general.autoExpandAIGroups ?? false}
296+
onChange={(v) => onGeneralToggle('autoExpandAIGroups', v)}
297+
disabled={saving}
298+
/>
299+
</SettingRow>
289300

290301
{isElectron && (
291302
<>

src/renderer/store/slices/sessionDetailSlice.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,15 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
416416
sessionPhaseInfo: phaseInfo,
417417
});
418418

419+
// Auto-expand all AI groups if the setting is enabled
420+
if (tabId && conversation?.items && get().appConfig?.general?.autoExpandAIGroups) {
421+
for (const item of conversation.items) {
422+
if (item.type === 'ai') {
423+
get().expandAIGroupForTab(tabId, item.group.id);
424+
}
425+
}
426+
}
427+
419428
// Store per-tab session data
420429
if (tabId) {
421430
const prev = get().tabSessionData;
@@ -554,6 +563,14 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
554563
}
555564
}
556565

566+
// Snapshot existing AI group IDs before overwriting state, so the
567+
// auto-expand diff below can correctly identify which groups are new.
568+
const prevGroupIds = new Set(
569+
(latestState.conversation?.items ?? [])
570+
.filter((item) => item.type === 'ai')
571+
.map((item) => (item as { type: 'ai'; group: { id: string } }).group.id)
572+
);
573+
557574
// Update only the data, preserve UI states
558575
set((state) => ({
559576
sessionDetail: detail,
@@ -572,6 +589,29 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
572589
// so expansion states are preserved
573590
}));
574591

592+
// Auto-expand newly arrived AI groups if the setting is enabled.
593+
// Uses prevGroupIds snapshotted before set() so the diff is accurate.
594+
if (get().appConfig?.general?.autoExpandAIGroups) {
595+
const oldGroupIds = prevGroupIds;
596+
const newGroupIds = newConversation.items
597+
.filter(
598+
(item) =>
599+
item.type === 'ai' &&
600+
!oldGroupIds.has((item as { type: 'ai'; group: { id: string } }).group.id)
601+
)
602+
.map((item) => (item as { type: 'ai'; group: { id: string } }).group.id);
603+
604+
if (newGroupIds.length > 0) {
605+
for (const tab of latestAllTabs) {
606+
if (tab.type === 'session' && tab.sessionId === sessionId) {
607+
for (const groupId of newGroupIds) {
608+
get().expandAIGroupForTab(tab.id, groupId);
609+
}
610+
}
611+
}
612+
}
613+
}
614+
575615
// Also update per-tab session data for all tabs viewing this session
576616
const latestTabSessionData = { ...get().tabSessionData };
577617
for (const tab of latestAllTabs) {

src/shared/types/notifications.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ export interface AppConfig {
262262
defaultTab: 'dashboard' | 'last-session';
263263
/** Optional custom Claude root folder (auto-detected when null) */
264264
claudeRootPath: string | null;
265+
/** Whether to auto-expand AI response groups when opening a transcript or receiving new messages */
266+
autoExpandAIGroups: boolean;
265267
};
266268
/** Display and UI settings */
267269
display: {

test/main/ipc/configValidation.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,28 @@ describe('configValidation', () => {
2020
}
2121
});
2222

23+
it('accepts general.autoExpandAIGroups boolean toggle', () => {
24+
const resultOn = validateConfigUpdatePayload('general', { autoExpandAIGroups: true });
25+
expect(resultOn.valid).toBe(true);
26+
if (resultOn.valid) {
27+
expect(resultOn.data).toEqual({ autoExpandAIGroups: true });
28+
}
29+
30+
const resultOff = validateConfigUpdatePayload('general', { autoExpandAIGroups: false });
31+
expect(resultOff.valid).toBe(true);
32+
if (resultOff.valid) {
33+
expect(resultOff.data).toEqual({ autoExpandAIGroups: false });
34+
}
35+
});
36+
37+
it('rejects non-boolean general.autoExpandAIGroups', () => {
38+
const result = validateConfigUpdatePayload('general', { autoExpandAIGroups: 'yes' });
39+
expect(result.valid).toBe(false);
40+
if (!result.valid) {
41+
expect(result.error).toContain('boolean');
42+
}
43+
});
44+
2345
it('accepts absolute general.claudeRootPath updates', () => {
2446
const result = validateConfigUpdatePayload('general', {
2547
claudeRootPath: '/Users/test/.claude',

0 commit comments

Comments
 (0)