feat: add memory limits UI settings for messages in session

This commit is contained in:
Bohdan Triapitsyn
2026-01-19 14:56:27 +02:00
parent 477c17f02c
commit 2743349b5e
11 changed files with 429 additions and 19 deletions
+13 -10
View File
@@ -5,7 +5,7 @@ import type { Message, Part } from "@opencode-ai/sdk/v2";
import { opencodeClient } from "@/lib/opencode/client";
import { isExecutionForkMetaText } from "@/lib/messages/executionMeta";
import type { SessionMemoryState, MessageStreamLifecycle, AttachedFile } from "./types/sessionTypes";
import { MEMORY_LIMITS } from "./types/sessionTypes";
import { MEMORY_LIMITS, getMemoryLimits } from "./types/sessionTypes";
import {
touchStreamingLifecycle,
removeLifecycleEntries,
@@ -385,10 +385,12 @@ export const useMessageStore = create<MessageStore>()(
pendingAssistantHeaderSessions: new Set(),
pendingUserMessageMetaBySession: new Map(),
loadMessages: async (sessionId: string, limit: number = MEMORY_LIMITS.HISTORICAL_MESSAGES) => {
loadMessages: async (sessionId: string, limit?: number) => {
const memLimits = getMemoryLimits();
const effectiveLimit = limit ?? memLimits.HISTORICAL_MESSAGES;
const isStreaming = get().sessionMemoryState.get(sessionId)?.isStreaming;
const targetLimit = isStreaming ? MEMORY_LIMITS.VIEWPORT_MESSAGES : limit;
const fetchLimit = isStreaming ? undefined : targetLimit + MEMORY_LIMITS.FETCH_BUFFER;
const targetLimit = isStreaming ? memLimits.VIEWPORT_MESSAGES : effectiveLimit;
const fetchLimit = isStreaming ? undefined : targetLimit + memLimits.FETCH_BUFFER;
const allMessages = await executeWithSessionDirectory(sessionId, () => opencodeClient.getSessionMessages(sessionId, fetchLimit));
// Filter out reverted messages first
@@ -2227,10 +2229,11 @@ export const useMessageStore = create<MessageStore>()(
};
},
trimToViewportWindow: (sessionId: string, targetSize: number = MEMORY_LIMITS.VIEWPORT_MESSAGES, currentSessionId?: string) => {
trimToViewportWindow: (sessionId: string, targetSize?: number, currentSessionId?: string) => {
const effectiveTargetSize = targetSize ?? getMemoryLimits().VIEWPORT_MESSAGES;
const state = get();
const sessionMessages = state.messages.get(sessionId);
if (!sessionMessages || sessionMessages.length <= targetSize) {
if (!sessionMessages || sessionMessages.length <= effectiveTargetSize) {
return;
}
@@ -2246,11 +2249,11 @@ export const useMessageStore = create<MessageStore>()(
}
const anchor = memoryState.viewportAnchor || sessionMessages.length - 1;
let start = Math.max(0, anchor - Math.floor(targetSize / 2));
const end = Math.min(sessionMessages.length, start + targetSize);
let start = Math.max(0, anchor - Math.floor(effectiveTargetSize / 2));
const end = Math.min(sessionMessages.length, start + effectiveTargetSize);
if (end === sessionMessages.length && end - start < targetSize) {
start = Math.max(0, end - targetSize);
if (end === sessionMessages.length && end - start < effectiveTargetSize) {
start = Math.max(0, end - effectiveTargetSize);
}
const trimmedMessages = sessionMessages.slice(start, end);
+31 -2
View File
@@ -51,7 +51,8 @@ export interface SessionContextUsage {
lastMessageId?: string;
}
export const MEMORY_LIMITS = {
// Default memory limits (can be overridden via settings)
export const DEFAULT_MEMORY_LIMITS = {
MAX_SESSIONS: 3,
VIEWPORT_MESSAGES: 120,
HISTORICAL_MESSAGES: 90,
@@ -61,7 +62,35 @@ export const MEMORY_LIMITS = {
ZOMBIE_TIMEOUT: 10 * 60 * 1000,
} as const;
export const ACTIVE_SESSION_WINDOW = 180;
export const DEFAULT_ACTIVE_SESSION_WINDOW = 180;
// Dynamic memory limits accessor - reads directly from UI store.
// NOTE: do not use require() here (breaks in browser/desktop runtime bundles).
import { useUIStore } from "../useUIStore";
export const getMemoryLimits = () => {
const state = useUIStore.getState?.();
if (!state) {
return DEFAULT_MEMORY_LIMITS;
}
return {
...DEFAULT_MEMORY_LIMITS,
HISTORICAL_MESSAGES: state.memoryLimitHistorical ?? DEFAULT_MEMORY_LIMITS.HISTORICAL_MESSAGES,
VIEWPORT_MESSAGES: state.memoryLimitViewport ?? DEFAULT_MEMORY_LIMITS.VIEWPORT_MESSAGES,
};
};
export const getActiveSessionWindow = () => {
const state = useUIStore.getState?.();
if (!state) {
return DEFAULT_ACTIVE_SESSION_WINDOW;
}
return state.memoryLimitActiveSession ?? DEFAULT_ACTIVE_SESSION_WINDOW;
};
// Legacy exports for backward compatibility (use getMemoryLimits() for dynamic values)
export const MEMORY_LIMITS = DEFAULT_MEMORY_LIMITS;
export const ACTIVE_SESSION_WINDOW = DEFAULT_ACTIVE_SESSION_WINDOW;
export type NewSessionDraftState = {
open: boolean;
+3 -3
View File
@@ -5,7 +5,7 @@ import type { Session, Message, Part } from "@opencode-ai/sdk/v2";
import type { PermissionRequest, PermissionResponse } from "@/types/permission";
import type { QuestionRequest } from "@/types/question";
import type { SessionStore, AttachedFile, EditPermissionMode } from "./types/sessionTypes";
import { ACTIVE_SESSION_WINDOW, MEMORY_LIMITS } from "./types/sessionTypes";
import { getActiveSessionWindow, getMemoryLimits } from "./types/sessionTypes";
import { useSessionStore as useSessionManagementStore } from "./sessionStore";
import { useMessageStore } from "./messageStore";
@@ -273,7 +273,7 @@ export const useSessionStore = create<SessionStore>()(
get().updateViewportAnchor(previousSessionId, previousMessages.length - 1);
}
get().trimToViewportWindow(previousSessionId, MEMORY_LIMITS.VIEWPORT_MESSAGES);
get().trimToViewportWindow(previousSessionId, getMemoryLimits().VIEWPORT_MESSAGES);
}
}
@@ -287,7 +287,7 @@ export const useSessionStore = create<SessionStore>()(
await get().loadMessages(id);
}
get().trimToViewportWindow(id, ACTIVE_SESSION_WINDOW);
get().trimToViewportWindow(id, getActiveSessionWindow());
// Analyze session messages to extract agent/model/variant choices
// This ensures context is available even when ModelControls isn't mounted
+27
View File
@@ -43,6 +43,9 @@ interface UIStore {
autoDeleteEnabled: boolean;
autoDeleteAfterDays: number;
autoDeleteLastRunAt: number | null;
memoryLimitHistorical: number;
memoryLimitViewport: number;
memoryLimitActiveSession: number;
toolCallExpansion: 'collapsed' | 'activity' | 'detailed';
fontSize: number;
@@ -87,6 +90,9 @@ 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;
setToolCallExpansion: (value: 'collapsed' | 'activity' | 'detailed') => void;
setFontSize: (size: number) => void;
setPadding: (size: number) => void;
@@ -141,6 +147,9 @@ export const useUIStore = create<UIStore>()(
autoDeleteEnabled: false,
autoDeleteAfterDays: 30,
autoDeleteLastRunAt: null,
memoryLimitHistorical: 90,
memoryLimitViewport: 120,
memoryLimitActiveSession: 180,
toolCallExpansion: 'collapsed',
fontSize: 100,
padding: 100,
@@ -296,6 +305,21 @@ export const useUIStore = create<UIStore>()(
set({ autoDeleteLastRunAt: timestamp });
},
setMemoryLimitHistorical: (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 });
},
setToolCallExpansion: (value) => {
set({ toolCallExpansion: value });
},
@@ -527,6 +551,9 @@ export const useUIStore = create<UIStore>()(
autoDeleteEnabled: state.autoDeleteEnabled,
autoDeleteAfterDays: state.autoDeleteAfterDays,
autoDeleteLastRunAt: state.autoDeleteLastRunAt,
memoryLimitHistorical: state.memoryLimitHistorical,
memoryLimitViewport: state.memoryLimitViewport,
memoryLimitActiveSession: state.memoryLimitActiveSession,
toolCallExpansion: state.toolCallExpansion,
fontSize: state.fontSize,
padding: state.padding,