release: prepare 0629001 portal updates

This commit is contained in:
john
2026-06-29 22:20:04 +08:00
parent 18ea4f82fd
commit a40e340a41
84 changed files with 17516 additions and 2475 deletions
+14 -13
View File
@@ -17,9 +17,6 @@ import { buildContextPrefix } from '../utils/mindspaceChatContext';
import { buildUserAddressPrefix } from '../utils/userAddress';
import {
CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
CHAT_IMAGE_MAX_SIDE,
compressImageForUpload,
} from '../utils/imageUpload';
import { buildAbsoluteAssetImageUrl } from '../utils/mindspaceCards';
import {
@@ -94,9 +91,8 @@ export function usePageEditSubChat({
const space = await getMindSpace();
const category =
space.categories.find((item) => item.code === 'private') ??
space.categories.find((item) => item.code === 'oa') ??
space.categories.find((item) => item.code === 'public') ??
space.categories.find((item) => item.code === 'oa') ??
space.categories[0];
if (!category) {
throw new Error('当前空间暂无可用的图片上传分类');
@@ -105,18 +101,23 @@ export function usePageEditSubChat({
return category.id;
}, []);
const uploadChatImage = useCallback(async (file: File): Promise<string> => {
const uploadChatImage = useCallback(async (
file: File,
onProgress?: (progress: number) => void,
): Promise<string> => {
if (!file.type.startsWith('image/')) {
throw new Error('只支持图片文件');
}
const compressed = await compressImageForUpload(file, {
maxInputBytes: CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
maxOutputBytes: CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
maxDimension: CHAT_IMAGE_MAX_SIDE,
});
const categoryId = await resolveChatImageUploadCategoryId();
const asset = await uploadMindSpaceAsset(categoryId, compressed);
return buildAbsoluteAssetImageUrl({ id: asset.id, updatedAt: asset.updatedAt });
const asset = await uploadMindSpaceAsset(categoryId, file, {
maxImageBytes: CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
onProgress,
});
return buildAbsoluteAssetImageUrl({
id: asset.id,
updatedAt: asset.updatedAt,
publicUrl: asset.publicUrl,
});
}, [resolveChatImageUploadCategoryId]);
const processEvent = useCallback((event: SessionEvent, requestId: string) => {
+319 -57
View File
@@ -10,10 +10,10 @@ import {
getMe,
listNotifications,
markNotificationRead,
getSession,
listSessions,
loadSessionDetail,
readConfig,
rememberUserMemory,
rememberProjectContext,
uploadMindSpaceAsset,
resumeSession,
@@ -21,6 +21,7 @@ import {
startSession,
subscribeNotificationEvents,
subscribeSessionEvents,
syncUserMemory,
updateProvider,
} from '../api/client';
import { appConfig } from '../config';
@@ -36,6 +37,7 @@ import type {
MindSpaceChatContext,
PortalUser,
Session,
SessionSummary,
SessionEvent,
ToolConfirmation,
UserNotification,
@@ -45,9 +47,6 @@ import { buildUserAddressPrefix } from '../utils/userAddress';
import { buildAutoChatSkillPrefix } from '../../chat-skills.mjs';
import {
CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
CHAT_IMAGE_MAX_SIDE,
compressImageForUpload,
} from '../utils/imageUpload';
import { buildAbsoluteAssetImageUrl } from '../utils/mindspaceCards';
import {
@@ -61,9 +60,10 @@ import {
pushMessage,
} from '../utils/message';
import {
mergeSessionLists,
appendSessionLists,
prependUnique,
shouldShowNewChatTitle,
toSessionSummary,
touchSession,
} from '../utils/sessions';
@@ -72,6 +72,21 @@ const REPLY_RECOVERY_SYNC_DELAYS_MS = [1500, 5000, 12000];
export { INSUFFICIENT_BALANCE_NOTICE };
function mergeMessagePages(older: Message[], current: Message[]): Message[] {
if (older.length === 0) return current;
if (current.length === 0) return older;
const seen = new Set<string>();
const merged: Message[] = [];
for (const message of [...older, ...current]) {
const key = message.id ?? `${message.role}:${message.created}:${merged.length}`;
if (seen.has(key)) continue;
seen.add(key);
merged.push(message);
}
return merged;
}
function isAmbiguousReplySubmitError(err: unknown) {
if (!(err instanceof ApiError)) return true;
return err.status === 0 || err.status === 409 || err.status >= 500;
@@ -84,15 +99,23 @@ export function useTKMindChat(
grantedSkills?: string[],
) {
const canUseProjectMemory = Boolean(capabilities?.context_memory);
const canUseLongTermMemory = Boolean(capabilities?.memory_store);
const [session, setSession] = useState<Session | null>(null);
const [sessions, setSessions] = useState<Session[]>([]);
const [sessions, setSessions] = useState<SessionSummary[]>([]);
const [sessionsLoading, setSessionsLoading] = useState(false);
const [sessionsLoadingMore, setSessionsLoadingMore] = useState(false);
const [sessionsHasMore, setSessionsHasMore] = useState(false);
const [sessionSearchQuery, setSessionSearchQueryState] = useState('');
const [messages, setMessages] = useState<Message[]>([]);
const [messageHistoryLoadingMore, setMessageHistoryLoadingMore] = useState(false);
const [messageHistoryHasMore, setMessageHistoryHasMore] = useState(false);
const [messageHistoryTotal, setMessageHistoryTotal] = useState(0);
const [chatState, setChatState] = useState<ChatState>('loading');
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [pendingTool, setPendingTool] = useState<ToolConfirmation | null>(null);
const [memoryLoading, setMemoryLoading] = useState(false);
const [userMemoryLoading, setUserMemoryLoading] = useState(false);
const [rechargePrompt, setRechargePrompt] = useState(false);
const [rechargeForced, setRechargeForced] = useState(false);
const [subscribePrompt, setSubscribePrompt] = useState(false);
@@ -104,7 +127,12 @@ export function useTKMindChat(
const connectTokenRef = useRef(0);
const messagesRef = useRef<Message[]>([]);
const sessionRef = useRef<Session | null>(null);
const sessionsRef = useRef<Session[]>([]);
const sessionsRef = useRef<SessionSummary[]>([]);
const sessionsOffsetRef = useRef(0);
const sessionSearchQueryRef = useRef('');
const messageHistoryLoadedCountRef = useRef(0);
const messageHistoryTotalRef = useRef(0);
const messageHistoryHasMoreRef = useRef(false);
const rememberedContextRef = useRef<string | null>(null);
const rememberInFlightRef = useRef(false);
const fallbackRetriedRef = useRef(new Set<string>());
@@ -187,6 +215,10 @@ export function useTKMindChat(
messagesRef.current = messages;
}, [messages]);
useEffect(() => {
messageHistoryHasMoreRef.current = messageHistoryHasMore;
}, [messageHistoryHasMore]);
useEffect(() => {
sessionRef.current = session;
}, [session]);
@@ -195,6 +227,10 @@ export function useTKMindChat(
sessionsRef.current = sessions;
}, [sessions]);
useEffect(() => {
sessionSearchQueryRef.current = sessionSearchQuery;
}, [sessionSearchQuery]);
useEffect(() => {
if (!user?.id) return;
let cancelled = false;
@@ -269,6 +305,27 @@ export function useTKMindChat(
}
}, [canUseProjectMemory]);
const formatUserMemoryNotice = useCallback((result: {
analyzed: number;
memories: number;
totalMemories: number;
syncedToSession: boolean;
}) => {
const fragments = [];
if (result.memories > 0) {
fragments.push(`新增 ${result.memories} 条长期记忆`);
} else if (result.analyzed > 0) {
fragments.push('已分析最近对话,暂无新的长期记忆');
} else {
fragments.push('没有新的用户对话可提炼');
}
fragments.push(`当前累计 ${result.totalMemories}`);
if (result.syncedToSession) {
fragments.push('已同步到当前会话');
}
return fragments.join('');
}, []);
const rememberRecentContext = useCallback(
async ({
silent = false,
@@ -345,28 +402,80 @@ export function useTKMindChat(
[canUseProjectMemory, loadProjectMemory],
);
const refreshSessions = useCallback(async () => {
setSessionsLoading(true);
const refreshSessions = useCallback(
async (options?: { preserveExisting?: boolean; query?: string }): Promise<SessionSummary[]> => {
setSessionsLoading(true);
try {
const { items, page } = await listSessions({
limit: appConfig.sessionPageSize,
offset: 0,
query: options?.query ?? sessionSearchQueryRef.current,
});
const merged = options?.preserveExisting ? appendSessionLists(sessionsRef.current, items) : items;
const nextOffset = Number(page.offset ?? 0) + items.length;
const total = Math.max(Number(page.total ?? merged.length), merged.length);
sessionsOffsetRef.current = options?.preserveExisting
? Math.max(sessionsOffsetRef.current, nextOffset)
: nextOffset;
setSessionsHasMore(merged.length < total);
if (options?.query != null) {
sessionSearchQueryRef.current = options.query;
setSessionSearchQueryState(options.query);
}
setSessions(merged);
return items;
} catch (err) {
if (err instanceof ApiError && err.status === 0) {
setError('网络不可用,无法刷新历史列表');
}
return [];
} finally {
setSessionsLoading(false);
}
},
[],
);
const loadMoreSessions = useCallback(async () => {
if (sessionsLoading || sessionsLoadingMore || !sessionsHasMore) return;
setSessionsLoadingMore(true);
try {
const items = await listSessions();
setSessions((prev) => mergeSessionLists(prev, items));
const { items, page } = await listSessions({
limit: appConfig.sessionPageSize,
offset: sessionsOffsetRef.current,
query: sessionSearchQueryRef.current,
});
const merged = appendSessionLists(sessionsRef.current, items);
const nextOffset = Number(page.offset ?? sessionsOffsetRef.current) + items.length;
const total = Math.max(Number(page.total ?? merged.length), merged.length);
sessionsOffsetRef.current = Math.max(sessionsOffsetRef.current, nextOffset);
setSessionsHasMore(merged.length < total);
setSessions(merged);
} catch (err) {
if (err instanceof ApiError && err.status === 0) {
setError('网络不可用,无法刷新历史列表');
setError('网络不可用,无法继续加载历史列表');
}
} finally {
setSessionsLoading(false);
setSessionsLoadingMore(false);
}
}, []);
}, [sessionsHasMore, sessionsLoading, sessionsLoadingMore]);
const setSessionSearchQuery = useCallback((query: string) => {
const nextQuery = query.trim();
sessionSearchQueryRef.current = nextQuery;
setSessionSearchQueryState(nextQuery);
sessionsOffsetRef.current = 0;
setSessionsHasMore(false);
void refreshSessions({ query: nextQuery });
}, [refreshSessions]);
const resolveChatImageUploadCategoryId = useCallback(async () => {
if (chatImageCategoryIdRef.current) return chatImageCategoryIdRef.current;
const space = await getMindSpace();
const category =
space.categories.find((item) => item.code === 'private') ??
space.categories.find((item) => item.code === 'oa') ??
space.categories.find((item) => item.code === 'public') ??
space.categories.find((item) => item.code === 'oa') ??
space.categories[0];
if (!category) {
throw new Error('当前空间暂无可用的图片上传分类');
@@ -376,32 +485,48 @@ export function useTKMindChat(
return category.id;
}, []);
const uploadChatImage = useCallback(async (file: File): Promise<string> => {
const uploadChatImage = useCallback(async (
file: File,
onProgress?: (progress: number) => void,
): Promise<string> => {
if (!file.type.startsWith('image/')) {
throw new Error('只支持图片文件');
}
const compressed = await compressImageForUpload(file, {
maxInputBytes: CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
maxOutputBytes: CHAT_IMAGE_UPLOAD_MAX_OUTPUT_BYTES,
maxDimension: CHAT_IMAGE_MAX_SIDE,
});
const categoryId = await resolveChatImageUploadCategoryId();
const asset = await uploadMindSpaceAsset(categoryId, compressed);
const asset = await uploadMindSpaceAsset(categoryId, file, {
maxImageBytes: CHAT_IMAGE_UPLOAD_MAX_INPUT_BYTES,
onProgress,
});
return buildAbsoluteAssetImageUrl({
id: asset.id,
updatedAt: asset.updatedAt,
publicUrl: asset.publicUrl,
});
}, [resolveChatImageUploadCategoryId]);
const syncSessionMessages = useCallback(async (sessionId: string) => {
try {
const detail = await loadSessionDetail(sessionId);
setSessions((prev) => prependUnique(prev, detail.session));
const currentLoadedCount = Math.max(
messageHistoryLoadedCountRef.current,
messagesRef.current.length,
appConfig.sessionMessagePageSize,
);
const detail = await loadSessionDetail(sessionId, undefined, {
before: 0,
limit: currentLoadedCount,
});
setSessions((prev) => prependUnique(prev, toSessionSummary(detail.session)));
if (sessionRef.current?.id !== sessionId) return;
messagesRef.current = detail.messages;
messageHistoryLoadedCountRef.current = detail.messages.length;
messageHistoryTotalRef.current = Math.max(Number(detail.page.total ?? detail.messages.length), detail.messages.length);
setMessageHistoryHasMore(detail.messages.length < messageHistoryTotalRef.current);
setMessageHistoryTotal(messageHistoryTotalRef.current);
setMessages(detail.messages);
setSession((current) => (current?.id === sessionId ? detail.session : current));
} catch {
} catch (err) {
console.warn('Session final sync failed:', err);
setNotice('对话已完成,但最终内容同步失败;当前显示可能仍是临时流式结果,请稍后刷新。');
// Keep the optimistic streamed state if the follow-up sync fails.
}
}, []);
@@ -468,6 +593,7 @@ export function useTKMindChat(
setMessages((prev) => {
const next = pushMessage(prev, event.message);
messagesRef.current = next;
messageHistoryLoadedCountRef.current = next.length;
return next;
});
const confirmation = getToolConfirmation(event.message);
@@ -480,10 +606,15 @@ export function useTKMindChat(
return;
}
case 'UpdateConversation': {
const snapshot = normalizeConversationMessages(
let snapshot = normalizeConversationMessages(
event.conversation.filter((m) => m.metadata?.userVisible),
);
const loadedCount = Math.max(messageHistoryLoadedCountRef.current, messagesRef.current.length);
if (messageHistoryHasMoreRef.current && loadedCount > 0 && snapshot.length > loadedCount) {
snapshot = snapshot.slice(-loadedCount);
}
messagesRef.current = mergeConversationSnapshot(messagesRef.current, snapshot);
messageHistoryLoadedCountRef.current = messagesRef.current.length;
setMessages(messagesRef.current);
return;
}
@@ -589,6 +720,12 @@ export function useTKMindChat(
setPendingTool(null);
activeRequestId.current = null;
messagesRef.current = [];
messageHistoryLoadedCountRef.current = 0;
messageHistoryTotalRef.current = 0;
messageHistoryHasMoreRef.current = false;
setMessageHistoryHasMore(false);
setMessageHistoryTotal(0);
setMessageHistoryLoadingMore(false);
setMessages([]);
setChatState('connecting');
}, []);
@@ -619,15 +756,29 @@ export function useTKMindChat(
const hints = knownSession
? { messageCount: knownSession.message_count, updatedAt: knownSession.updated_at }
: undefined;
const { session: detail, messages: history } = await loadSessionDetail(sessionId, hints);
const { session: detail, messages: history, page } = await loadSessionDetail(
sessionId,
hints,
{
before: 0,
limit: appConfig.sessionMessagePageSize,
},
);
if (token !== connectTokenRef.current) return;
writeStoredSessionId(userRef.current?.id, sessionId);
setSession({ ...detail, ...(resumed ?? {}), id: sessionId });
messagesRef.current = history;
messageHistoryLoadedCountRef.current = history.length;
messageHistoryTotalRef.current = Math.max(Number(page.total ?? history.length), history.length);
messageHistoryHasMoreRef.current = history.length < messageHistoryTotalRef.current;
setMessageHistoryHasMore(messageHistoryHasMoreRef.current);
setMessageHistoryTotal(messageHistoryTotalRef.current);
setMessages(history);
setChatState('idle');
setSessions((prev) => prependUnique(prev, { ...detail, ...resumed, id: sessionId }));
setSessions((prev) =>
prependUnique(prev, toSessionSummary({ ...detail, ...(resumed ?? {}), id: sessionId })),
);
subscribeToSession(sessionId);
},
@@ -646,6 +797,43 @@ export function useTKMindChat(
}
}, []);
const loadOlderMessages = useCallback(async () => {
const sessionId = sessionRef.current?.id;
if (!sessionId || messageHistoryLoadingMore || !messageHistoryHasMoreRef.current) return;
const knownSession = sessionsRef.current.find((item) => item.id === sessionId);
const hints = knownSession
? { messageCount: knownSession.message_count, updatedAt: knownSession.updated_at }
: undefined;
setMessageHistoryLoadingMore(true);
try {
const { messages: olderMessages, page } = await loadSessionDetail(
sessionId,
hints,
{
before: messageHistoryLoadedCountRef.current,
limit: appConfig.sessionMessagePageSize,
},
);
if (sessionRef.current?.id !== sessionId) return;
const merged = mergeMessagePages(olderMessages, messagesRef.current);
messagesRef.current = merged;
messageHistoryLoadedCountRef.current = merged.length;
messageHistoryTotalRef.current = Math.max(Number(page.total ?? merged.length), merged.length);
messageHistoryHasMoreRef.current = merged.length < messageHistoryTotalRef.current;
setMessageHistoryHasMore(messageHistoryHasMoreRef.current);
setMessageHistoryTotal(messageHistoryTotalRef.current);
setMessages(merged);
} catch (err) {
if (err instanceof ApiError && err.status === 0) {
setError('网络不可用,无法继续加载更早消息');
}
} finally {
setMessageHistoryLoadingMore(false);
}
}, [messageHistoryLoadingMore]);
const retryConnect = useCallback(async () => {
const sessionId = session?.id ?? readStoredSessionId(userRef.current?.id);
if (!sessionId) return;
@@ -669,45 +857,54 @@ export function useTKMindChat(
setChatState('loading');
setError(null);
const summaries = await refreshSessions();
const previousSessionId = readStoredSessionId(userRef.current?.id);
let staleSession = false;
let restorableSessionId: string | null = null;
if (previousSessionId) {
try {
const previousSession = await getSession(previousSessionId);
if (shouldShowNewChatTitle(previousSession)) {
try {
await deleteChatSession(previousSessionId);
} catch {
// Best-effort cleanup: a failed delete should not block the next fresh chat.
}
clearStoredSessionId(userRef.current?.id);
} else {
restorableSessionId = previousSessionId;
}
} catch (err) {
if (err instanceof ApiError && (err.status === 403 || err.status === 404 || err.status === 400)) {
clearStoredSessionId(userRef.current?.id);
staleSession = true;
} else {
throw err;
const previousSession = summaries.find((item) => item.id === previousSessionId) ?? null;
if (previousSession && shouldShowNewChatTitle(previousSession)) {
try {
await deleteChatSession(previousSessionId);
} catch {
// Best-effort cleanup: a failed delete should not block the next fresh chat.
}
clearStoredSessionId(userRef.current?.id);
setSessions((prev) => prev.filter((item) => item.id !== previousSessionId));
} else {
restorableSessionId = previousSessionId;
}
}
if (staleSession) {
setNotice('上次会话已失效,已为你新建对话');
}
if (cancelled) return;
if (restorableSessionId) {
await connectSessionRef.current(restorableSessionId, { showLoading: false });
try {
await connectSessionRef.current(restorableSessionId, { showLoading: false });
} catch (err) {
if (err instanceof ApiError && (err.status === 403 || err.status === 404 || err.status === 400)) {
clearStoredSessionId(userRef.current?.id);
staleSession = true;
setSession(null);
messageHistoryLoadedCountRef.current = 0;
messageHistoryTotalRef.current = 0;
messageHistoryHasMoreRef.current = false;
setMessageHistoryHasMore(false);
setMessageHistoryTotal(0);
setMessages([]);
setChatState('idle');
} else {
throw err;
}
}
if (cancelled) return;
} else {
setChatState('idle');
}
void refreshSessions();
if (staleSession) {
setNotice('上次会话已失效,已为你新建对话');
}
} catch (err) {
if (!cancelled) {
if (err instanceof ApiError && err.status === 402) {
@@ -732,7 +929,7 @@ export function useTKMindChat(
useEffect(() => {
const handleVisibility = () => {
if (!document.hidden && session?.id) {
void refreshSessions();
void refreshSessions({ preserveExisting: true });
}
};
document.addEventListener('visibilitychange', handleVisibility);
@@ -747,7 +944,11 @@ export function useTKMindChat(
const token = connectTokenRef.current;
const target = sessions.find((item) => item.id === sessionId);
if (target) {
setSession(target);
setSession({
...target,
working_dir: userRef.current?.workspaceRoot ?? '',
conversation: null,
});
writeStoredSessionId(userRef.current?.id, sessionId);
}
@@ -794,6 +995,7 @@ export function useTKMindChat(
const requestId = crypto.randomUUID();
activeRequestId.current = requestId;
messagesRef.current = [...messagesRef.current, userMessage];
messageHistoryLoadedCountRef.current = messagesRef.current.length;
setMessages(messagesRef.current);
setChatState('streaming');
setError(null);
@@ -807,7 +1009,7 @@ export function useTKMindChat(
activeSessionId = created.id;
writeStoredSessionId(userRef.current?.id, created.id);
setSession(created);
setSessions((prev) => prependUnique(prev, created));
setSessions((prev) => prependUnique(prev, toSessionSummary(created)));
subscribeToSession(created.id);
void ensureProvider(created.id);
void loadProjectMemory(created.id, false);
@@ -894,6 +1096,12 @@ export function useTKMindChat(
clearStoredSessionId(userRef.current?.id);
activeRequestId.current = null;
messagesRef.current = [];
messageHistoryLoadedCountRef.current = 0;
messageHistoryTotalRef.current = 0;
messageHistoryHasMoreRef.current = false;
setMessageHistoryHasMore(false);
setMessageHistoryTotal(0);
setMessageHistoryLoadingMore(false);
setMessages([]);
setSession(null);
setError(null);
@@ -906,13 +1114,53 @@ export function useTKMindChat(
await loadProjectMemory(session.id, true);
}, [chatState, loadProjectMemory, session]);
const rememberCurrentUserMemory = useCallback(async () => {
if (!session || !canUseLongTermMemory) return;
if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
setUserMemoryLoading(true);
try {
const result = await rememberUserMemory(session.id);
setNotice(formatUserMemoryNotice(result));
} catch (err) {
setNotice(`保存长期记忆失败:${err instanceof Error ? err.message : String(err)}`);
} finally {
setUserMemoryLoading(false);
}
}, [canUseLongTermMemory, chatState, formatUserMemoryNotice, session]);
const refreshUserMemory = useCallback(async () => {
if (!session || !canUseLongTermMemory) return;
if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
setUserMemoryLoading(true);
try {
const result = await syncUserMemory(session.id);
setNotice(
result.syncedToSession
? `长期记忆已同步(累计 ${result.totalMemories} 条)`
: '长期记忆已刷新,但当前会话尚未同步',
);
} catch (err) {
setNotice(`刷新长期记忆失败:${err instanceof Error ? err.message : String(err)}`);
} finally {
setUserMemoryLoading(false);
}
}, [canUseLongTermMemory, chatState, session]);
const rememberCurrentContext = useCallback(async () => {
if (chatState === 'streaming' || chatState === 'loading' || chatState === 'connecting') return;
await rememberRecentContext({ silent: false });
}, [chatState, rememberRecentContext]);
const onSidebarOpen = useCallback(() => {
void refreshSessions();
if (sessionSearchQueryRef.current) {
void refreshSessions({ query: sessionSearchQueryRef.current });
return;
}
if (sessionsRef.current.length === 0) {
void refreshSessions();
return;
}
void refreshSessions({ preserveExisting: true });
}, [refreshSessions]);
const deleteSession = useCallback(
@@ -950,21 +1198,35 @@ export function useTKMindChat(
session,
sessions,
sessionsLoading,
sessionsLoadingMore,
sessionsHasMore,
sessionSearchQuery,
messages,
messageHistoryLoadingMore,
messageHistoryHasMore,
messageHistoryTotal,
chatState,
error,
notice,
pendingTool,
memoryLoading,
userMemoryLoading,
canUseProjectMemory,
canUseLongTermMemory,
submit,
stop,
approveTool,
newSession,
rememberCurrentContext,
refreshProjectMemory,
rememberCurrentUserMemory,
refreshUserMemory,
switchSession,
deleteSession,
refreshSessions,
loadMoreSessions,
setSessionSearchQuery,
loadOlderMessages,
retryConnect,
dismissNotice,
onSidebarOpen,