refactor: message list optimizations and single messageLimit setting (#409)

* feat: message list refactoring and optimizations

* refactor: collapse 3 memory-limit settings into single messageLimit

- Replace memoryLimitHistorical/Viewport/ActiveSession with one `messageLimit` (default 200)
- Background trim derived automatically (limit * 0.6), not user-facing
- Fix stale historyLimit overriding current messageLimit on session reload
- Remove streaming cap that reduced fetch to VIEWPORT_MESSAGES during active streams
- Deduplicate detectTurns() call in TurnGroupingContext (reuse staticValue.turns)
- Zustand v3 migration: collapse old fields, preserve user-customized values
- Server: sanitize single messageLimit field (10-500)
This commit is contained in:
Bohdan Triapitsyn
2026-02-12 20:36:02 +02:00
committed by GitHub
parent 3bf0843ebb
commit 252d4bd5ab
13 changed files with 627 additions and 330 deletions
+42 -37
View File
@@ -88,9 +88,7 @@ interface UIStore {
autoDeleteEnabled: boolean;
autoDeleteAfterDays: number;
autoDeleteLastRunAt: number | null;
memoryLimitHistorical: number;
memoryLimitViewport: number;
memoryLimitActiveSession: number;
messageLimit: number;
toolCallExpansion: 'collapsed' | 'activity' | 'detailed';
fontSize: number;
@@ -172,9 +170,7 @@ interface UIStore {
setAutoDeleteEnabled: (value: boolean) => void;
setAutoDeleteAfterDays: (days: number) => void;
setAutoDeleteLastRunAt: (timestamp: number | null) => void;
setMemoryLimitHistorical: (value: number) => void;
setMemoryLimitViewport: (value: number) => void;
setMemoryLimitActiveSession: (value: number) => void;
setMessageLimit: (value: number) => void;
setToolCallExpansion: (value: 'collapsed' | 'activity' | 'detailed') => void;
setFontSize: (size: number) => void;
setTerminalFontSize: (size: number) => void;
@@ -256,9 +252,7 @@ export const useUIStore = create<UIStore>()(
autoDeleteEnabled: false,
autoDeleteAfterDays: 30,
autoDeleteLastRunAt: null,
memoryLimitHistorical: 90,
memoryLimitViewport: 120,
memoryLimitActiveSession: 180,
messageLimit: 200,
toolCallExpansion: 'collapsed',
fontSize: 100,
terminalFontSize: 13,
@@ -568,19 +562,9 @@ export const useUIStore = create<UIStore>()(
set({ autoDeleteLastRunAt: timestamp });
},
setMemoryLimitHistorical: (value) => {
setMessageLimit: (value) => {
const clamped = Math.max(10, Math.min(500, Math.round(value)));
set({ memoryLimitHistorical: clamped });
},
setMemoryLimitViewport: (value) => {
const clamped = Math.max(20, Math.min(500, Math.round(value)));
set({ memoryLimitViewport: clamped });
},
setMemoryLimitActiveSession: (value) => {
const clamped = Math.max(30, Math.min(1000, Math.round(value)));
set({ memoryLimitActiveSession: clamped });
set({ messageLimit: clamped });
},
setToolCallExpansion: (value) => {
@@ -881,24 +865,47 @@ export const useUIStore = create<UIStore>()(
{
name: 'ui-store',
storage: createJSONStorage(() => getSafeStorage()),
version: 1,
version: 3,
migrate: (persistedState, version) => {
if (version >= 1 || !persistedState || typeof persistedState !== 'object') {
if (!persistedState || typeof persistedState !== 'object') {
return persistedState;
}
const state = persistedState as Record<string, unknown>;
if (!isLegacyDefaultTemplates(state.notificationTemplates)) {
return persistedState;
// v0 -> v1: reset legacy notification templates
if (version < 1) {
if (isLegacyDefaultTemplates(state.notificationTemplates)) {
state.notificationTemplates = {
completion: { ...EMPTY_NOTIFICATION_TEMPLATES.completion },
error: { ...EMPTY_NOTIFICATION_TEMPLATES.error },
question: { ...EMPTY_NOTIFICATION_TEMPLATES.question },
subtask: { ...EMPTY_NOTIFICATION_TEMPLATES.subtask },
};
}
}
return {
...state,
notificationTemplates: {
completion: { ...EMPTY_NOTIFICATION_TEMPLATES.completion },
error: { ...EMPTY_NOTIFICATION_TEMPLATES.error },
question: { ...EMPTY_NOTIFICATION_TEMPLATES.question },
subtask: { ...EMPTY_NOTIFICATION_TEMPLATES.subtask },
},
};
// v2 -> v3: collapse 3 memory-limit fields into single messageLimit.
// Pick the best user-customised value (prefer historical, fall back to active).
// Discard old defaults (90/120/180) — they become the new single default (200).
if (version < 3) {
const OLD_DEFAULTS = new Set([90, 120, 180, 220]);
const hist = state.memoryLimitHistorical as number | undefined;
const active = state.memoryLimitActiveSession as number | undefined;
// If user had a non-default custom value, keep it as the new messageLimit.
if (typeof hist === 'number' && !OLD_DEFAULTS.has(hist)) {
state.messageLimit = hist;
} else if (typeof active === 'number' && !OLD_DEFAULTS.has(active)) {
state.messageLimit = active;
}
// Otherwise leave undefined → Zustand uses the initial default (200).
delete state.memoryLimitHistorical;
delete state.memoryLimitViewport;
delete state.memoryLimitActiveSession;
}
return state;
},
partialize: (state) => ({
theme: state.theme,
@@ -918,9 +925,7 @@ export const useUIStore = create<UIStore>()(
autoDeleteEnabled: state.autoDeleteEnabled,
autoDeleteAfterDays: state.autoDeleteAfterDays,
autoDeleteLastRunAt: state.autoDeleteLastRunAt,
memoryLimitHistorical: state.memoryLimitHistorical,
memoryLimitViewport: state.memoryLimitViewport,
memoryLimitActiveSession: state.memoryLimitActiveSession,
messageLimit: state.messageLimit,
toolCallExpansion: state.toolCallExpansion,
fontSize: state.fontSize,
terminalFontSize: state.terminalFontSize,