Merge pull request #59 from proxikal/feat/auto-expand-ai-groups-setting
feat: add auto-expand AI response groups setting
This commit is contained in:
commit
dc38c79c23
9 changed files with 91 additions and 1 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}` };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ export function useSettingsHandlers({
|
|||
theme: 'dark',
|
||||
defaultTab: 'dashboard',
|
||||
claudeRootPath: null,
|
||||
autoExpandAIGroups: false,
|
||||
},
|
||||
display: {
|
||||
showTimestamps: true,
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue