feat: add auto-expand AI response groups setting

- Add toggle in settings to auto-expand AI response groups
- Auto-expand new AI groups on live session refresh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
proxy 2026-02-22 19:13:28 -05:00
parent 9c04e90fdd
commit 93b515af40
9 changed files with 91 additions and 1 deletions

View file

@ -7,6 +7,9 @@ The format is based on Keep a Changelog and this project follows Semantic Versio
## [Unreleased]
### Added
- `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.
- Strict IPC input validation guards for project/session/subagent/search limits.
- `get-waterfall-data` IPC endpoint implementation.
- Cross-platform path normalization in renderer path resolvers.

View file

@ -203,6 +203,7 @@ function validateGeneralSection(data: unknown): ValidationSuccess<'general'> | V
'theme',
'defaultTab',
'claudeRootPath',
'autoExpandAIGroups',
];
const result: Partial<GeneralConfig> = {};
@ -267,6 +268,12 @@ function validateGeneralSection(data: unknown): ValidationSuccess<'general'> | V
result.claudeRootPath = path.resolve(normalized);
}
break;
case 'autoExpandAIGroups':
if (typeof value !== 'boolean') {
return { valid: false, error: `general.${key} must be a boolean` };
}
result.autoExpandAIGroups = value;
break;
default:
return { valid: false, error: `Unsupported general key: ${key}` };
}

View file

@ -181,6 +181,7 @@ export interface GeneralConfig {
theme: 'dark' | 'light' | 'system';
defaultTab: 'dashboard' | 'last-session';
claudeRootPath: string | null;
autoExpandAIGroups: boolean;
}
export interface DisplayConfig {
@ -248,6 +249,7 @@ const DEFAULT_CONFIG: AppConfig = {
theme: 'dark',
defaultTab: 'dashboard',
claudeRootPath: null,
autoExpandAIGroups: false,
},
display: {
showTimestamps: true,

View file

@ -30,6 +30,7 @@ export interface SafeConfig {
theme: 'dark' | 'light' | 'system';
defaultTab: 'dashboard' | 'last-session';
claudeRootPath: string | null;
autoExpandAIGroups: boolean;
};
notifications: {
enabled: boolean;
@ -154,6 +155,7 @@ export function useSettingsConfig(): UseSettingsConfigReturn {
theme: displayConfig?.general?.theme ?? 'dark',
defaultTab: displayConfig?.general?.defaultTab ?? 'dashboard',
claudeRootPath: displayConfig?.general?.claudeRootPath ?? null,
autoExpandAIGroups: displayConfig?.general?.autoExpandAIGroups ?? false,
},
notifications: {
enabled: displayConfig?.notifications?.enabled ?? true,

View file

@ -287,6 +287,7 @@ export function useSettingsHandlers({
theme: 'dark',
defaultTab: 'dashboard',
claudeRootPath: null,
autoExpandAIGroups: false,
},
display: {
showTimestamps: true,

View file

@ -15,6 +15,7 @@ import { SettingRow, SettingsSectionHeader, SettingsSelect, SettingsToggle } fro
import type { SafeConfig } from '../hooks/useSettingsConfig';
import type { ClaudeRootInfo, WslClaudeRootCandidate } from '@shared/types';
import type { HttpServerStatus } from '@shared/types/api';
import type { AppConfig } from '@shared/types/notifications';
// Theme options
const THEME_OPTIONS = [
@ -26,7 +27,7 @@ const THEME_OPTIONS = [
interface GeneralSectionProps {
readonly safeConfig: SafeConfig;
readonly saving: boolean;
readonly onGeneralToggle: (key: 'launchAtLogin' | 'showDockIcon', value: boolean) => void;
readonly onGeneralToggle: (key: keyof AppConfig['general'], value: boolean) => void;
readonly onThemeChange: (value: 'dark' | 'light' | 'system') => void;
}
@ -286,6 +287,16 @@ export const GeneralSection = ({
disabled={saving}
/>
</SettingRow>
<SettingRow
label="Expand AI responses by default"
description="Automatically expand each response turn when opening a transcript or receiving a new message"
>
<SettingsToggle
enabled={safeConfig.general.autoExpandAIGroups ?? false}
onChange={(v) => onGeneralToggle('autoExpandAIGroups', v)}
disabled={saving}
/>
</SettingRow>
{isElectron && (
<>

View file

@ -416,6 +416,15 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
sessionPhaseInfo: phaseInfo,
});
// Auto-expand all AI groups if the setting is enabled
if (tabId && conversation?.items && get().appConfig?.general?.autoExpandAIGroups) {
for (const item of conversation.items) {
if (item.type === 'ai') {
get().expandAIGroupForTab(tabId, item.group.id);
}
}
}
// Store per-tab session data
if (tabId) {
const prev = get().tabSessionData;
@ -554,6 +563,14 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
}
}
// Snapshot existing AI group IDs before overwriting state, so the
// auto-expand diff below can correctly identify which groups are new.
const prevGroupIds = new Set(
(latestState.conversation?.items ?? [])
.filter((item) => item.type === 'ai')
.map((item) => (item as { type: 'ai'; group: { id: string } }).group.id)
);
// Update only the data, preserve UI states
set((state) => ({
sessionDetail: detail,
@ -572,6 +589,29 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
// so expansion states are preserved
}));
// Auto-expand newly arrived AI groups if the setting is enabled.
// Uses prevGroupIds snapshotted before set() so the diff is accurate.
if (get().appConfig?.general?.autoExpandAIGroups) {
const oldGroupIds = prevGroupIds;
const newGroupIds = newConversation.items
.filter(
(item) =>
item.type === 'ai' &&
!oldGroupIds.has((item as { type: 'ai'; group: { id: string } }).group.id)
)
.map((item) => (item as { type: 'ai'; group: { id: string } }).group.id);
if (newGroupIds.length > 0) {
for (const tab of latestAllTabs) {
if (tab.type === 'session' && tab.sessionId === sessionId) {
for (const groupId of newGroupIds) {
get().expandAIGroupForTab(tab.id, groupId);
}
}
}
}
}
// Also update per-tab session data for all tabs viewing this session
const latestTabSessionData = { ...get().tabSessionData };
for (const tab of latestAllTabs) {

View file

@ -262,6 +262,8 @@ export interface AppConfig {
defaultTab: 'dashboard' | 'last-session';
/** Optional custom Claude root folder (auto-detected when null) */
claudeRootPath: string | null;
/** Whether to auto-expand AI response groups when opening a transcript or receiving new messages */
autoExpandAIGroups: boolean;
};
/** Display and UI settings */
display: {

View file

@ -20,6 +20,28 @@ describe('configValidation', () => {
}
});
it('accepts general.autoExpandAIGroups boolean toggle', () => {
const resultOn = validateConfigUpdatePayload('general', { autoExpandAIGroups: true });
expect(resultOn.valid).toBe(true);
if (resultOn.valid) {
expect(resultOn.data).toEqual({ autoExpandAIGroups: true });
}
const resultOff = validateConfigUpdatePayload('general', { autoExpandAIGroups: false });
expect(resultOff.valid).toBe(true);
if (resultOff.valid) {
expect(resultOff.data).toEqual({ autoExpandAIGroups: false });
}
});
it('rejects non-boolean general.autoExpandAIGroups', () => {
const result = validateConfigUpdatePayload('general', { autoExpandAIGroups: 'yes' });
expect(result.valid).toBe(false);
if (!result.valid) {
expect(result.error).toContain('boolean');
}
});
it('accepts absolute general.claudeRootPath updates', () => {
const result = validateConfigUpdatePayload('general', {
claudeRootPath: '/Users/test/.claude',